G4 ELA · Smart-board edition
The Year of the Rat · Lesson Packet
Tap a lesson to open it full screen. Use “← Contents” at the top left of a lesson to come back here. Sheets are independent — teach them in any order.
Vocabulary & Background — use before reading
Reading & Comprehension
Grammar — story sentences, not workbook items
Spelling
Writing
Free Writing — from the Connect to Writing prompts
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"a398c2ea-c158-49a4-8ef2-88eb42e7665f": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "b6f27813-3f4d-4e62-b0ad-976ffbc91d93": {"mime": "image/png", "compressed": false, "data": "@@f67d5a848c0ea2@@"}, "b58af936-e829-443e-b6bc-0148c4b96560": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "5bb60148-bc07-4a6a-b31b-5db78e96297a": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "7fe19f7a-8d83-40e2-a842-c614effb9f4a": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "8e986afb-06fd-4700-beb1-38b046f9587a": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "9f78387b-6e98-415c-bcb8-844db7346f90": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "48268b45-8e43-4740-b0a9-7345baa809d7": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "db87a330-ffb3-4a27-b12e-6a78a2c703fb": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "f7e41715-f1cc-4ad1-9919-9eaa4646e140": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "114ea174-f8b3-45d6-bd98-04fa77545b09": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "5df46249-c6c3-40ba-be24-64416fe45657": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "3139faf2-f90f-4872-bd38-3c3f165a8d75": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "c1e033f4-97ba-40a3-aca6-831f2651d28c": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "4ba1bb37-804a-4196-812a-9db95ae8adc9": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "8e23b77f-61b7-4247-bb1d-9f9e4e558457": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "5a933218-59b9-467e-8989-ae2692c208e0": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "374a808b-ced3-445b-b355-28e2ba201979": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "dd09fd41-d8a3-48c5-9509-fac14bd3f1a6": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "4d4f9fd3-62a5-4df2-97e0-1be9920f4094": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "22a4c490-6a31-4881-be32-851b31e4a959": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "02c8a44f-28f8-4887-a77c-86d522c24792": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "a6fd8a2a-9580-4deb-9e57-62fd4b245eae": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "30991569-20df-4271-b37b-8597cc2d7aff": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "9bacf36e-b719-4128-a8f1-1294dc46c6ae": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "e3112214-ff15-44a6-ab05-7901501ce5dd": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f00c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f00c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f00c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f00c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"5bb60148-bc07-4a6a-b31b-5db78e96297a\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7fe19f7a-8d83-40e2-a842-c614effb9f4a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8e986afb-06fd-4700-beb1-38b046f9587a\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"9f78387b-6e98-415c-bcb8-844db7346f90\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"48268b45-8e43-4740-b0a9-7345baa809d7\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"db87a330-ffb3-4a27-b12e-6a78a2c703fb\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7fe19f7a-8d83-40e2-a842-c614effb9f4a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8e986afb-06fd-4700-beb1-38b046f9587a\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"9f78387b-6e98-415c-bcb8-844db7346f90\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"48268b45-8e43-4740-b0a9-7345baa809d7\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"db87a330-ffb3-4a27-b12e-6a78a2c703fb\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f7e41715-f1cc-4ad1-9919-9eaa4646e140\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"114ea174-f8b3-45d6-bd98-04fa77545b09\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5df46249-c6c3-40ba-be24-64416fe45657\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3139faf2-f90f-4872-bd38-3c3f165a8d75\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c1e033f4-97ba-40a3-aca6-831f2651d28c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4ba1bb37-804a-4196-812a-9db95ae8adc9\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8e23b77f-61b7-4247-bb1d-9f9e4e558457\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f7e41715-f1cc-4ad1-9919-9eaa4646e140\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"114ea174-f8b3-45d6-bd98-04fa77545b09\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5df46249-c6c3-40ba-be24-64416fe45657\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3139faf2-f90f-4872-bd38-3c3f165a8d75\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c1e033f4-97ba-40a3-aca6-831f2651d28c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4ba1bb37-804a-4196-812a-9db95ae8adc9\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8e23b77f-61b7-4247-bb1d-9f9e4e558457\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5a933218-59b9-467e-8989-ae2692c208e0\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"374a808b-ced3-445b-b355-28e2ba201979\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"dd09fd41-d8a3-48c5-9509-fac14bd3f1a6\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4d4f9fd3-62a5-4df2-97e0-1be9920f4094\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"22a4c490-6a31-4881-be32-851b31e4a959\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"02c8a44f-28f8-4887-a77c-86d522c24792\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a6fd8a2a-9580-4deb-9e57-62fd4b245eae\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5a933218-59b9-467e-8989-ae2692c208e0\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"374a808b-ced3-445b-b355-28e2ba201979\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"dd09fd41-d8a3-48c5-9509-fac14bd3f1a6\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4d4f9fd3-62a5-4df2-97e0-1be9920f4094\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"22a4c490-6a31-4881-be32-851b31e4a959\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"02c8a44f-28f8-4887-a77c-86d522c24792\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a6fd8a2a-9580-4deb-9e57-62fd4b245eae\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"30991569-20df-4271-b37b-8597cc2d7aff\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"9bacf36e-b719-4128-a8f1-1294dc46c6ae\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"e3112214-ff15-44a6-ab05-7901501ce5dd\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"b58af936-e829-443e-b6bc-0148c4b96560\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 1 · Vocabulary — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Words We Need<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"a398c2ea-c158-49a4-8ef2-88eb42e7665f\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 16<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1180px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-6); font-size: 19px;\">Before you start: you do not need to have read the story yet.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-2);\"><em>relying<\u002Fem> — students who know the rule “change y to i” often write <em>reliing<\u002Fem>. The y stays before -ing. <em>clumsy<\u002Fem> — the u sound tempts an o.<\u002Fp>\n <p style=\"margin: 0;\">Five of the Part A words (<em>resolutions, doubts, relying, clumsy, awkward<\u002Fem>) are the anthology’s critical vocabulary. The other three carry the chapter’s ideas and are worth pre-teaching.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Match each word to its meaning<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">8 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-2);\">Every one of these words does a job in the chapter: Pacy makes <em>resolutions<\u002Fem> at Chinese New Year; she has <em>doubts<\u002Fem> about her future; the tiger crosses the water by <em>relying<\u002Fem> on himself; her first drawings feel <em>clumsy<\u002Fem>; things are <em>awkward<\u002Fem> and <em>strained<\u002Fem> with Becky and Charlotte; Dad’s story is about two <em>philosophers<\u002Fem>; and the panda Pacy draws is <em>endangered<\u002Fem>.<\u002Fp>\n <p style=\"margin: 0;\">Ask for the word’s job before matching: “Who relies on something in this story?” A meaning you can attach to a person sticks better than a definition.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Tap a word, then tap its meaning. A correct pair locks in.<\u002Fp>\n <div style=\"display: grid; grid-template-columns: minmax(240px, 320px) 1fr; gap: var(--space-8); align-items: start;\">\n <div style=\"display: flex; flex-direction: column; gap: var(--space-3);\">\n <sc-for list=\"{{ words }}\" as=\"w\" hint-placeholder-count=\"8\">\n <button sc-camel-on-click=\"{{ w.pick }}\" style=\"text-align: left; font-family: var(--font-body); font-size: 32px; font-weight: 600; padding: 12px 18px; min-height: 52px; border-radius: var(--radius-md); cursor: pointer; background: {{ w.bg }}; border: {{ w.border }}; color: {{ w.color }};\">\n <span style=\"font-variant-numeric: tabular-nums; font-size: 19px; color: var(--color-neutral-600); font-family: var(--font-body); margin-right: 10px;\">{{ w.n }}<\u002Fspan>{{ w.text }}<span style=\"color: var(--color-accent-700);\">{{ w.tick }}<\u002Fspan>\n <\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-3);\">\n <sc-for list=\"{{ meanings }}\" as=\"m\" hint-placeholder-count=\"8\">\n <div>\n <button sc-camel-on-click=\"{{ m.pick }}\" style=\"width: 100%; text-align: left; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: 12px 18px; min-height: 52px; border-radius: var(--radius-md); cursor: pointer; display: flex; align-items: center; gap: var(--space-4); background: {{ m.bg }}; border: {{ m.border }}; animation: {{ m.anim }};\">\n <span style=\"font-family: var(--font-heading); font-style: italic; font-size: 22px; color: var(--color-neutral-600); width: 16px;\">{{ m.letter }}<\u002Fspan>\n <span style=\"flex: 1;\">{{ m.text }}<\u002Fspan>\n <span style=\"font-family: var(--font-body); font-weight: 600; font-size: 24px; color: var(--color-accent-700); white-space: nowrap;\">{{ m.badge }}<\u002Fspan>\n <\u002Fbutton>\n <sc-if value=\"{{ m.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; padding: 6px 18px 0;\">{{ m.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Fill in the missing letter<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">8 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainB }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainB }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Say the word aloud <em>before<\u002Fem> choosing — the missing letter is usually the one the mouth hides. The two traps: <em>relying<\u002Fem> keeps its y before -ing (no “change y to i” here — that rule is for -es and -ed), and <em>clumsy<\u002Fem> sounds like an o but takes a u. Every word in this part comes from the story.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Tap the missing letter. The whole word appears when it is right.<\u002Fp>\n <div style=\"display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--space-4);\">\n <sc-for list=\"{{ bWords }}\" as=\"b\" hint-placeholder-count=\"8\">\n <div style=\"border: {{ b.border }}; border-radius: var(--radius-md); padding: var(--space-4); animation: {{ b.anim }};\">\n <div style=\"display: flex; align-items: baseline; gap: 5px; letter-spacing: 2px; font-family: var(--font-body); font-size: 30px; font-weight: 600; margin-bottom: var(--space-3); flex-wrap: wrap;\">\n <span style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600); font-family: var(--font-body); letter-spacing: 0; margin-right: 6px;\">{{ b.n }}.<\u002Fspan>\n <span>{{ b.pre }}<\u002Fspan>\n <span style=\"display: inline-block; min-width: 26px; border-bottom: 2px solid {{ b.blankColor }}; text-align: center; color: var(--color-accent-700);\">{{ b.fill }}<\u002Fspan>\n <span>{{ b.post }}<\u002Fspan>\n <\u002Fdiv>\n <sc-if value=\"{{ b.open }}\" hint-placeholder-val=\"{{ true }}\">\n <div style=\"display: flex; gap: var(--space-2); flex-wrap: wrap;\">\n <sc-for list=\"{{ b.opts }}\" as=\"o\" hint-placeholder-count=\"6\">\n <button sc-camel-on-click=\"{{ o.pick }}\" style=\"min-width: 44px; min-height: 44px; font-family: var(--font-body); font-size: 26px; font-weight: 600; border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; cursor: pointer; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent); background: var(--color-accent-100);\">{{ o.letter }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsc-if>\n <sc-if value=\"{{ b.done }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-family: var(--font-body); font-style: italic; font-size: 26px; color: var(--color-accent-700);\">✓ {{ b.full }}<\u002Fdiv>\n <\u002Fsc-if>\n <sc-if value=\"{{ b.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 18px; margin-top: var(--space-2);\">{{ b.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <p style=\"margin: var(--space-4) 0 0; font-size: 17px; color: var(--color-neutral-600); font-style: italic;\">Every word in Part B comes from the story. Five of them are your critical vocabulary words.<\u002Fp>\n <\u002Fsection>\n <img data-deco=\"\" src=\"b6f27813-3f4d-4e62-b0ad-976ffbc91d93\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { sel: null, matched: {}, tries: {}, shakeM: null, b: {}, bTries: {}, shakeB: null, earned: 0, teacher: false, explainA: false, explainB: false };\n PAIRS = { resolutions: 'c', doubts: 'g', relying: 'h', clumsy: 'e', awkward: 'a', philosopher: 'd', endangered: 'b', strained: 'f' };\n MEANINGS = [\n ['a', 'tense and uncomfortable', 'Think of the lunch table with Becky and Charlotte — nobody knows what to say.'],\n ['b', 'in danger of disappearing from the world forever', 'Pacy draws an animal like this for her project.'],\n ['c', 'promises to do or not do something', 'People make these at New Year.'],\n ['d', 'a person who thinks deeply and asks big questions about life', 'Dad tells a story about two of them on a bridge.'],\n ['e', 'done in a careless way', 'How Pacy feels about her early drawings.'],\n ['f', 'stretched thin — not easy or friendly any more', 'What happened to an old friendship.'],\n ['g', 'the feeling of not being sure about something', 'What Pacy has about her future.'],\n ['h', 'trusting or depending on someone', 'What the tiger did — on his own courage.'],\n ];\n BWORDS = [\n ['awkw', 'rd', 'a', 'awkward', 'Say it slowly: awk-ward.'],\n ['cl', 'msy', 'u', 'clumsy', 'It sounds like an o — but it is a u.'],\n ['cour', 'ge', 'a', 'courage', 'Like \"cabbage\" and \"village\" — the ending is -age.'],\n ['do', 'bts', 'u', 'doubts', 'The b is silent, and an o-u pair comes before it.'],\n ['illustr', 'tor', 'a', 'illustrator', 'Someone who illustrates — keep the a from \"illustrate\".'],\n ['rel', 'ing', 'y', 'relying', 'The y stays before -ing. \"Change y to i\" is for -es and -ed.'],\n ['resol', 'tion', 'u', 'resolution', 'Hear the \"oo\" in the middle: res-o-LU-tion.'],\n ['fright', 'ned', 'e', 'frightened', 'frighten + ed — the e belongs to \"frighten\".'],\n ];\n OPTS = ['a', 'e', 'i', 'o', 'u', 'y'];\n shake(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const wordNames = Object.keys(this.PAIRS);\n const words = wordNames.map((w, i) => {\n const m = !!s.matched[this.PAIRS[w]], sel = s.sel === w;\n return { n: i + 1, text: w, tick: m ? ' \\u2713' : '',\n bg: sel ? 'var(--color-accent-100)' : 'transparent',\n border: sel ? '1px solid var(--color-accent)' : m ? '1px solid var(--color-accent-300)' : '1px solid var(--color-divider)',\n color: m ? 'var(--color-neutral-600)' : 'var(--color-text)',\n pick: () => { if (!m) this.setState({ sel: sel ? null : w }); } };\n });\n const byLetter = {}; wordNames.forEach(w => byLetter[this.PAIRS[w]] = w);\n const meanings = this.MEANINGS.map(([letter, text, hint]) => {\n const m = !!s.matched[letter];\n return { letter, text, hint, badge: m ? byLetter[letter] : '',\n bg: m ? 'var(--color-accent-100)' : 'transparent',\n border: m ? '1px solid var(--color-accent-300)' : '1px solid var(--color-divider)',\n anim: s.shakeM === letter ? 'omShake 0.4s' : 'none',\n showHint: !m && (s.tries[letter] || 0) >= 2,\n pick: () => {\n if (m || !s.sel) return;\n if (this.PAIRS[s.sel] === letter) this.setState(p => ({ matched: { ...p.matched, [letter]: true }, sel: null, earned: p.earned + 1 }));\n else { this.setState(p => ({ tries: { ...p.tries, [letter]: (p.tries[letter] || 0) + 1 } })); this.shake('shakeM', letter); }\n } };\n });\n const bWords = this.BWORDS.map(([pre, post, ans, full, hint], i) => {\n const done = !!s.b[i];\n return { n: i + 1, pre, post, full, hint, done, open: !done,\n fill: done ? ans : '\\u00a0',\n blankColor: done ? 'var(--color-accent)' : 'var(--color-neutral-400)',\n border: done ? '1px solid var(--color-accent-300)' : '1px solid var(--color-divider)',\n anim: s.shakeB === i ? 'omShake 0.4s' : 'none',\n showHint: !done && (s.bTries[i] || 0) >= 2,\n opts: this.OPTS.map(letter => ({ letter, pick: () => {\n if (letter === ans) this.setState(p => ({ b: { ...p.b, [i]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ bTries: { ...p.bTries, [i]: (p.bTries[i] || 0) + 1 } })); this.shake('shakeB', i); }\n } })) };\n });\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA, explainB: s.explainB,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n toggleExplainB: () => this.setState(p => ({ explainB: !p.explainB })),\n words, meanings, bWords };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"17d9138b-64c8-4d19-8463-d9f2a2e3e2e0": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "cdf13bed-c2ab-4ddd-9f53-fe6821e1a855": {"mime": "image/jpeg", "compressed": false, "data": "@@4346f924f586c6@@"}, "40ac22e4-7408-42fb-bfb4-d04e22da7d64": {"mime": "image/png", "compressed": false, "data": "@@3c3bf92f3d3396@@"}, "61baa725-69a7-427a-acce-203a81e282b5": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "c4a27c8e-7132-4c1a-a5d2-36eb25b3bcbe": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "c3ff406b-43db-4bde-9ec6-5372e2768006": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "2cd590d2-9bed-4fae-99e9-b88c8941ed90": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "7e7b163e-ab6e-46c8-8072-ccde64d4d29e": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "551dfc26-6233-4b6a-b21a-36cc7e71ade4": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "4307cecc-17f3-43fc-adec-668e0c82b45a": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "7c34f088-e641-4603-bfd1-1be5ccace4f2": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "15bfba97-2819-4abc-aa65-c78a5a5c4c78": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "ede3484a-9f6d-4ea6-9bf5-c3b3d4a1a3ef": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "06ec1fce-38e1-49bd-8932-d59f9024f5b6": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "743c00b3-572a-42f7-8bc1-4c68465ae3fc": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "b300de7d-f3e5-4933-a011-fcac3460fc1f": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "d72067b0-6dda-4201-b365-f97fbeacbafb": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "0502f572-de2c-4c4c-8e07-203e4d1e7948": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "ff8fadfe-540d-4576-9c72-923d1b85400e": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "5b89776b-e08e-4e8e-86f2-ecd65825e2b6": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "abcd5868-f957-4414-985b-c7dd9e2ed1ed": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "daea394a-c404-4c46-b51f-23bdfc040b7d": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "bc4b798f-544a-446c-bb8d-097981b21b51": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "e57f8c0d-63ff-4285-8eb4-74e7e8b42079": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "1469b4c0-2832-47a1-bc4b-5f7ab631b0b6": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "a0d9d2e4-e51f-4620-bfbd-2eadbe50a410": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "279e3cff-d102-48b3-9b56-6a041fd2a75f": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f01c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f01c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f01c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f01c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"c4a27c8e-7132-4c1a-a5d2-36eb25b3bcbe\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c3ff406b-43db-4bde-9ec6-5372e2768006\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2cd590d2-9bed-4fae-99e9-b88c8941ed90\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7e7b163e-ab6e-46c8-8072-ccde64d4d29e\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"551dfc26-6233-4b6a-b21a-36cc7e71ade4\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4307cecc-17f3-43fc-adec-668e0c82b45a\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c3ff406b-43db-4bde-9ec6-5372e2768006\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2cd590d2-9bed-4fae-99e9-b88c8941ed90\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7e7b163e-ab6e-46c8-8072-ccde64d4d29e\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"551dfc26-6233-4b6a-b21a-36cc7e71ade4\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4307cecc-17f3-43fc-adec-668e0c82b45a\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7c34f088-e641-4603-bfd1-1be5ccace4f2\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"15bfba97-2819-4abc-aa65-c78a5a5c4c78\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ede3484a-9f6d-4ea6-9bf5-c3b3d4a1a3ef\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"06ec1fce-38e1-49bd-8932-d59f9024f5b6\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"743c00b3-572a-42f7-8bc1-4c68465ae3fc\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b300de7d-f3e5-4933-a011-fcac3460fc1f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d72067b0-6dda-4201-b365-f97fbeacbafb\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7c34f088-e641-4603-bfd1-1be5ccace4f2\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"15bfba97-2819-4abc-aa65-c78a5a5c4c78\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ede3484a-9f6d-4ea6-9bf5-c3b3d4a1a3ef\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"06ec1fce-38e1-49bd-8932-d59f9024f5b6\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"743c00b3-572a-42f7-8bc1-4c68465ae3fc\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b300de7d-f3e5-4933-a011-fcac3460fc1f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d72067b0-6dda-4201-b365-f97fbeacbafb\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0502f572-de2c-4c4c-8e07-203e4d1e7948\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ff8fadfe-540d-4576-9c72-923d1b85400e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5b89776b-e08e-4e8e-86f2-ecd65825e2b6\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"abcd5868-f957-4414-985b-c7dd9e2ed1ed\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"daea394a-c404-4c46-b51f-23bdfc040b7d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"bc4b798f-544a-446c-bb8d-097981b21b51\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e57f8c0d-63ff-4285-8eb4-74e7e8b42079\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0502f572-de2c-4c4c-8e07-203e4d1e7948\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ff8fadfe-540d-4576-9c72-923d1b85400e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5b89776b-e08e-4e8e-86f2-ecd65825e2b6\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"abcd5868-f957-4414-985b-c7dd9e2ed1ed\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"daea394a-c404-4c46-b51f-23bdfc040b7d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"bc4b798f-544a-446c-bb8d-097981b21b51\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e57f8c0d-63ff-4285-8eb4-74e7e8b42079\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"1469b4c0-2832-47a1-bc4b-5f7ab631b0b6\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a0d9d2e4-e51f-4620-bfbd-2eadbe50a410\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"279e3cff-d102-48b3-9b56-6a041fd2a75f\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"61baa725-69a7-427a-acce-203a81e282b5\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 2 · Reading — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Before You Read: Genre, Setting & Background<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"17d9138b-64c8-4d19-8463-d9f2a2e3e2e0\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: you do not need to have read the story yet.<\u002Fp>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-6); font-size: 21px;\"><strong style=\"font-family: var(--font-heading);\">Realistic fiction<\u002Fstrong> tells a story about characters and events that are like those in real life.<\u002Fdiv>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0;\">Students who answer C1 and C2 the same way. The whole point is that Dad’s joke has two meanings — that is what makes Pacy start thinking. In Part A, the false items name other genres: 2 is “completely true” (that is non-fiction), 4 is fantasy, 6 is historical fiction.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Tick only the true sentences<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">4 points · there are four true ones<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Realistic fiction is <em>invented but believable<\u002Fem>: it could happen, but it did not have to. The three false sentences each describe a different genre — “completely true” is non-fiction, “magic and talking animals” is fantasy, “a hundred years ago” is historical fiction. Before tapping, ask of each sentence: <em>could this happen on our street?<\u002Fem><\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Tap a sentence to tick it. Only true ones will take the tick.<\u002Fp>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-3);\">\n <sc-for list=\"{{ aItems }}\" as=\"a\" hint-placeholder-count=\"7\">\n <div>\n <button sc-camel-on-click=\"{{ a.pick }}\" style=\"width: 100%; text-align: left; display: flex; align-items: center; gap: var(--space-4); font-family: var(--font-body); font-size: 24px; line-height: 1.5; padding: 12px 18px; min-height: 54px; border-radius: var(--radius-md); cursor: pointer; background: {{ a.bg }}; border: {{ a.border }}; animation: {{ a.anim }};\">\n <span style=\"width: 32px; height: 32px; flex: none; border: 1.5px solid {{ a.boxColor }}; border-radius: var(--radius-sm, 4px); display: inline-flex; align-items: center; justify-content: center; font-size: 26px; color: var(--color-accent-700);\">{{ a.tick }}<\u002Fspan>\n <span>{{ a.text }}<\u002Fspan>\n <\u002Fbutton>\n <sc-if value=\"{{ a.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; padding: 6px 18px 0;\">{{ a.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Background: the Chinese zodiac<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">3 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainB }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainB }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">The Chinese zodiac is a twelve-year cycle, one animal per year — so your animal comes from the year you were born, not the month. The Year of the Rat starts the cycle over, which is why it is said to bring <em>change and new beginnings<\u002Fem>. Pacy is a <em>tiger<\u002Fem> — hold that fact; the whole chapter turns on it.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Tap a blank, then tap the word that belongs in it. Two words in the bank belong nowhere.<\u002Fp>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-4);\">\n <sc-for list=\"{{ bItems }}\" as=\"b\" hint-placeholder-count=\"3\">\n <div style=\"font-size: 24px; line-height: 1.6; animation: {{ b.anim }};\">\n <span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ b.n }}.<\u002Fspan>\n <span>{{ b.pre }}<\u002Fspan>\n <button sc-camel-on-click=\"{{ b.pick }}\" style=\"display: inline-block; min-width: 120px; min-height: 40px; margin: 0 4px; padding: 2px 14px; font-family: var(--font-body); font-weight: 600; font-size: 26px; color: var(--color-accent-700); background: {{ b.bg }}; border: none; border-bottom: 2px solid {{ b.lineColor }}; border-radius: var(--radius-sm, 4px) var(--radius-sm, 4px) 0 0; cursor: pointer; vertical-align: baseline;\">{{ b.fill }}<\u002Fbutton>\n <span>{{ b.post }}<\u002Fspan>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-3); flex-wrap: wrap; border-top: 1px solid var(--color-divider); padding-top: var(--space-4);\">\n <sc-for list=\"{{ bank }}\" as=\"k\" hint-placeholder-count=\"6\">\n <button sc-camel-on-click=\"{{ k.pick }}\" style=\"min-height: 48px; padding: 8px 22px; font-family: var(--font-body); font-size: 26px; font-weight: 600; border: 1px solid var(--color-divider); border-radius: var(--radius-md); cursor: pointer; background: transparent; color: {{ k.color }}; text-decoration: {{ k.deco }};\" style-hover=\"border-color: var(--color-accent); background: var(--color-accent-100);\">{{ k.word }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">C<\u002Fspan>The two cold doors<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">3 points · talk first, then write<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Pacy’s dad uses the words <em>cold door<\u002Fem> in two different ways. Talk it out, type the class answer, then reveal the model. Award the point when the class has it.<\u002Fp>\n <div style=\"display: grid; grid-template-columns: 250px 1fr; gap: var(--space-6); align-items: start;\">\n <figure class=\"plate\" style=\"margin: 0;\"><img src=\"cdf13bed-c2ab-4ddd-9f53-fe6821e1a855\" alt=\"Pacy at the cold door\" style=\"width: 100%; display: block;\"><\u002Ffigure>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ cItems }}\" as=\"c\" hint-placeholder-count=\"3\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ c.n }}.<\u002Fspan>{{ c.q }}<\u002Fdiv>\n <textarea rows=\"2\" placeholder=\"Type the class answer here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3); align-items: center;\">\n <button sc-camel-on-click=\"{{ c.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ c.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ c.award }}\" style=\"background: {{ c.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-text); cursor: pointer;\">{{ c.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ c.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px; line-height: 1.65; color: var(--color-neutral-800);\"><em style=\"color: var(--color-accent-700);\">Model:<\u002Fem> {{ c.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"40ac22e4-7408-42fb-bfb4-d04e22da7d64\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 180px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { aDone: {}, aTries: {}, shakeA: null, bSel: null, bDone: {}, shakeB: null, cRevealed: {}, cAwarded: {}, earned: 0, teacher: false, explainA: false, explainB: false };\n A = [\n ['It tells a story about characters and events that could happen in real life.', true, ''],\n ['Everything in it must be completely true.', false, 'That would be non-fiction. Realistic fiction is invented — just believable.'],\n ['It uses dialogue — conversations between characters.', true, ''],\n ['It has magic, talking animals, and impossible events.', false, 'That is fantasy. Could it happen on our street?'],\n ['The main character usually learns a lesson.', true, ''],\n ['It must be set at least a hundred years ago.', false, 'That is historical fiction. This story happens now.'],\n ['It is sometimes told by a character in the story, using \\u201cI\\u201d.', true, ''],\n ];\n B = [\n ['In the Chinese zodiac, each year is named after an', '.', 'animal'],\n ['The Year of the Rat is believed to bring', 'and new beginnings.', 'change'],\n ['Pacy was born in the year of the', '.', 'tiger'],\n ];\n BANK = ['animal', 'luck', 'tiger', 'rat', 'change', 'dragon'];\n C = [\n ['The real cold door is\\u2026', 'The real door of the house, which lets in freezing air on the snow day.'],\n ['The other \\u201ccold door\\u201d he means is\\u2026', 'Becoming an author and illustrator \\u2014 a job Dad thinks is difficult and leads to a hard life.'],\n ['Why do you think he calls a difficult job a cold door?', 'Any sensible idea: a cold door is unwelcoming; you do not want to open it; you do not know what is on the other side.'],\n ];\n shake(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const aItems = this.A.map(([text, isTrue, hint], i) => {\n const done = !!s.aDone[i];\n return { text, hint, tick: done ? '\\u2713' : '\\u00a0', showHint: (s.aTries[i] || 0) >= 1 && !done,\n bg: done ? 'var(--color-accent-100)' : 'transparent',\n border: done ? '1px solid var(--color-accent-300)' : '1px solid var(--color-divider)',\n boxColor: done ? 'var(--color-accent)' : 'var(--color-neutral-400)',\n anim: s.shakeA === i ? 'omShake 0.4s' : 'none',\n pick: () => {\n if (done) return;\n if (isTrue) this.setState(p => ({ aDone: { ...p.aDone, [i]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ aTries: { ...p.aTries, [i]: (p.aTries[i] || 0) + 1 } })); this.shake('shakeA', i); }\n } };\n });\n const bItems = this.B.map(([pre, post, ans], i) => {\n const done = !!s.bDone[i], sel = s.bSel === i;\n return { n: i + 1, pre, post,\n fill: done ? ans : '\\u00a0',\n bg: sel ? 'var(--color-accent-100)' : 'transparent',\n lineColor: done ? 'var(--color-accent)' : sel ? 'var(--color-accent)' : 'var(--color-neutral-400)',\n anim: s.shakeB === i ? 'omShake 0.4s' : 'none',\n pick: () => { if (!done) this.setState({ bSel: sel ? null : i }); } };\n });\n const used = {}; this.B.forEach(([,, ans], i) => { if (s.bDone[i]) used[ans] = true; });\n const bank = this.BANK.map(word => ({ word,\n color: used[word] ? 'var(--color-neutral-400)' : 'var(--color-text)',\n deco: used[word] ? 'line-through' : 'none',\n pick: () => {\n const i = s.bSel;\n if (i == null || used[word]) return;\n if (this.B[i][2] === word) this.setState(p => ({ bDone: { ...p.bDone, [i]: true }, bSel: null, earned: p.earned + 1 }));\n else this.shake('shakeB', i);\n } }));\n const cItems = this.C.map(([q, model], i) => {\n const revealed = !!s.cRevealed[i], awarded = !!s.cAwarded[i];\n return { n: i + 1, q, model, revealed,\n revealLabel: revealed ? 'Hide model' : 'Reveal model',\n awardLabel: awarded ? '\\u2713 Point awarded' : 'Award point',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ cRevealed: { ...p.cRevealed, [i]: !revealed } })),\n award: () => this.setState(p => ({ cAwarded: { ...p.cAwarded, [i]: !awarded }, earned: p.earned + (awarded ? -1 : 1) })) };\n });\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA, explainB: s.explainB,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n toggleExplainB: () => this.setState(p => ({ explainB: !p.explainB })),\n aItems, bItems, bank, cItems };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"20a34bb4-0700-42fd-bfbc-c056291cc399": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "84134b6a-81fc-435d-b424-b2eaadb1c0f6": {"mime": "image/jpeg", "compressed": false, "data": "@@c9e3157db338b3@@"}, "6f6b61a3-e417-4c0b-954f-2d4ff685c431": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "7d8f2ad7-faf2-4bf3-b4ce-9fd0fef17fa5": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "afc52ba9-d62c-48c5-a46c-05fe826513b3": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "2c79460e-cf4e-4c3c-abc9-9e09c189ad0e": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "d7c1ee1c-4496-4b83-90e4-f64a048b5cbf": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "b75591db-947d-4038-b01a-28c8e96bf5b1": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "4abad68e-86bf-41e8-af23-359a871d47ae": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "209f67f7-b528-456b-b748-dc7d2c3c4886": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "3316db00-4ac0-4d39-9e45-d40f8de6f2b6": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "2c0d8261-2cac-4d1b-83d9-96bafa84c49d": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "3b620f0c-16b0-40eb-9762-82ed0a0a4481": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "7805f20e-29b9-4944-b5de-41c468d61b1d": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "0a8d0554-faff-49f6-b2a7-bdd899297a01": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "ef05dbd6-a230-4949-bed2-4994012e41cf": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "ac0aed0f-23a2-45f8-8de9-97f04ff1c2d3": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "4493e28f-e66c-4697-9a9b-1754887579a3": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "3fad28af-c5eb-4fe4-a474-eb7cc833aea0": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "cdefe09f-b843-4043-b890-dc302e5f16ce": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "b2958307-2b9d-4ce3-86a4-64dd4714609e": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "f4565fe5-43db-4792-95d3-2b2ba3f68958": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "972e823d-ab2c-4e5b-857f-c949d63cc895": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "9bf21836-cae3-4669-aac9-3f871ff29e39": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "b0d381cf-3fc8-4f6f-93cc-9df0d9c08fca": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "58cb9bd4-6fa0-497e-898a-dbfc15681f44": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f02c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f02c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f02c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f02c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"7d8f2ad7-faf2-4bf3-b4ce-9fd0fef17fa5\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"afc52ba9-d62c-48c5-a46c-05fe826513b3\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2c79460e-cf4e-4c3c-abc9-9e09c189ad0e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d7c1ee1c-4496-4b83-90e4-f64a048b5cbf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b75591db-947d-4038-b01a-28c8e96bf5b1\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4abad68e-86bf-41e8-af23-359a871d47ae\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"afc52ba9-d62c-48c5-a46c-05fe826513b3\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2c79460e-cf4e-4c3c-abc9-9e09c189ad0e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d7c1ee1c-4496-4b83-90e4-f64a048b5cbf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b75591db-947d-4038-b01a-28c8e96bf5b1\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4abad68e-86bf-41e8-af23-359a871d47ae\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"209f67f7-b528-456b-b748-dc7d2c3c4886\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3316db00-4ac0-4d39-9e45-d40f8de6f2b6\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2c0d8261-2cac-4d1b-83d9-96bafa84c49d\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3b620f0c-16b0-40eb-9762-82ed0a0a4481\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7805f20e-29b9-4944-b5de-41c468d61b1d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0a8d0554-faff-49f6-b2a7-bdd899297a01\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ef05dbd6-a230-4949-bed2-4994012e41cf\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"209f67f7-b528-456b-b748-dc7d2c3c4886\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3316db00-4ac0-4d39-9e45-d40f8de6f2b6\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2c0d8261-2cac-4d1b-83d9-96bafa84c49d\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3b620f0c-16b0-40eb-9762-82ed0a0a4481\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7805f20e-29b9-4944-b5de-41c468d61b1d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0a8d0554-faff-49f6-b2a7-bdd899297a01\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ef05dbd6-a230-4949-bed2-4994012e41cf\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ac0aed0f-23a2-45f8-8de9-97f04ff1c2d3\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4493e28f-e66c-4697-9a9b-1754887579a3\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3fad28af-c5eb-4fe4-a474-eb7cc833aea0\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cdefe09f-b843-4043-b890-dc302e5f16ce\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b2958307-2b9d-4ce3-86a4-64dd4714609e\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f4565fe5-43db-4792-95d3-2b2ba3f68958\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"972e823d-ab2c-4e5b-857f-c949d63cc895\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ac0aed0f-23a2-45f8-8de9-97f04ff1c2d3\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4493e28f-e66c-4697-9a9b-1754887579a3\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3fad28af-c5eb-4fe4-a474-eb7cc833aea0\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"cdefe09f-b843-4043-b890-dc302e5f16ce\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b2958307-2b9d-4ce3-86a4-64dd4714609e\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f4565fe5-43db-4792-95d3-2b2ba3f68958\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"972e823d-ab2c-4e5b-857f-c949d63cc895\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"9bf21836-cae3-4669-aac9-3f871ff29e39\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b0d381cf-3fc8-4f6f-93cc-9df0d9c08fca\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"58cb9bd4-6fa0-497e-898a-dbfc15681f44\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"6f6b61a3-e417-4c0b-954f-2d4ff685c431\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 3 · Reading — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Meet the People in the Story<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"20a34bb4-0700-42fd-bfbc-c056291cc399\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 15<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: read the whole story. Answer, then prove it — give the paragraph number!<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0;\">1 point per cell, 15 total. Accept any defensible answer with a paragraph citation — the citation habit is the real goal here. An answer with no ¶ scores half. Award each character’s points with the 1–5 row under the card.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">✎<\u002Fspan>Three people, four questions each<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">15 points · 3 per row, 1 per person<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplain }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explain }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Notice the pattern: one person <em>hands the choice back to Pacy<\u002Fem> (Dad), one <em>doubts her<\u002Fem> (Becky & Charlotte), and Pacy herself moves from unsure to brave. An answer without a paragraph number is only half an answer — “prove it” is the skill this sheet trains.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-4); align-items: start;\">\n <sc-for list=\"{{ chars }}\" as=\"ch\" hint-placeholder-count=\"3\">\n <div style=\"border: 1.5px solid {{ ch.hex }}; border-radius: var(--radius-md); padding: var(--space-4); min-width: 0;\">\n <div style=\"font-family: var(--font-heading); font-size: 28px; font-weight: 600; color: {{ ch.deep }}; border-bottom: 2px solid {{ ch.hex }}; padding-bottom: var(--space-2); margin-bottom: var(--space-3);\">{{ ch.name }}<\u002Fdiv>\n <sc-for list=\"{{ ch.rows }}\" as=\"r\" hint-placeholder-count=\"4\">\n <div style=\"margin-bottom: var(--space-3);\">\n <div style=\"font-size: 17px; color: var(--color-neutral-700); margin-bottom: 4px;\">{{ r.label }}<\u002Fdiv>\n <textarea rows=\"2\" placeholder=\"Class answer…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 20px; line-height: 1.5; padding: var(--space-2); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <\u002Fdiv>\n <\u002Fsc-for>\n <div style=\"display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3);\">\n <span style=\"font-size: 17px; color: var(--color-neutral-700);\">¶ proof:<\u002Fspan>\n <textarea rows=\"1\" placeholder=\"¶…\" style=\"flex: 1; box-sizing: border-box; font-family: var(--font-body); font-size: 20px; padding: var(--space-2); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: none;\"><\u002Ftextarea>\n <\u002Fdiv>\n <button sc-camel-on-click=\"{{ ch.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer; margin-bottom: var(--space-3);\" style-hover=\"background: var(--color-accent-100);\">{{ ch.revealLabel }}<\u002Fbutton>\n <sc-if value=\"{{ ch.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border-top: 1px solid var(--color-divider); padding-top: var(--space-3); margin-bottom: var(--space-3); font-size: 18px; line-height: 1.6;\">\n <sc-for list=\"{{ ch.models }}\" as=\"m\" hint-placeholder-count=\"4\">\n <p style=\"margin: 0 0 var(--space-2);\"><em style=\"color: {{ ch.deep }};\">{{ m.label }}<\u002Fem> {{ m.text }}<\u002Fp>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; border-top: 1px solid var(--color-divider); padding-top: var(--space-3);\">\n <span style=\"font-size: 17px; color: var(--color-neutral-700);\">Points:<\u002Fspan>\n <sc-for list=\"{{ ch.pts }}\" as=\"p\" hint-placeholder-count=\"5\">\n <button sc-camel-on-click=\"{{ p.pick }}\" style=\"width: 44px; height: 44px; border-radius: 50%; font-family: var(--font-body); font-size: 20px; font-variant-numeric: tabular-nums; border: 1.5px solid {{ p.border }}; background: {{ p.bg }}; color: {{ p.color }}; cursor: pointer;\">{{ p.n }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"84134b6a-81fc-435d-b424-b2eaadb1c0f6\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { revealed: {}, awarded: { 0: 0, 1: 0, 2: 0 }, teacher: false, explain: false };\n CHARS = [\n { name: 'Pacy', hex: '#33517a', deep: '#263d5c', models: [\n ['Who:', 'The narrator; born in the year of the tiger; wants to be an author and illustrator.'],\n ['Believes about her future:', 'Unsure at first \\u2014 is it a wishful dream or something she can make happen?'],\n ['Says:', '\\u201cI know I\\u2019m going to be one, no matter what.\\u201d'],\n ['By the end:', 'Feels brave \\u2014 hears a tiger roar inside her.'],\n ['\\u00b6:', '\\u00b64\\u20135, 24, 30\\u201332, 46'],\n ] },\n { name: 'Dad', hex: '#b5372f', deep: '#8e2a24', models: [\n ['Who:', 'Pacy\\u2019s father; tells her stories and gives advice.'],\n ['Believes about her future:', 'It is a \\u201ccold door\\u201d: difficult, leading to a hard life.'],\n ['Says:', '\\u201cOnly you can decide, because only you know who you are.\\u201d'],\n ['By the end:', 'Does not change; his story about the tiger is what frees her.'],\n ['\\u00b6:', '\\u00b611\\u201313, 19, 22\\u201323'],\n ] },\n { name: 'Becky & Charlotte', hex: '#c9973f', deep: '#7c5520', models: [\n ['Who:', 'Two classmates; Pacy\\u2019s friends before Melody arrived.'],\n ['Believes about her future:', 'Doubt it \\u2014 \\u201cWhat if you don\\u2019t become an author and illustrator?\\u201d'],\n ['Says:', '\\u201cThen you would have done all that practicing for nothing.\\u201d'],\n ['By the end:', 'Becky agrees to stop saying \\u201cDumb-Way\\u201d; they are invited to a sleepover.'],\n ['\\u00b6:', '\\u00b628\\u201329, 35, 38, 41\\u201344'],\n ] },\n ];\n ROWS = ['Who are they?', 'What do they believe about Pacy\\u2019s future?', 'One thing they say that matters', 'How do things change by the end?'];\n renderVals() {\n const s = this.state;\n const chars = this.CHARS.map((c, ci) => {\n const revealed = !!s.revealed[ci], cur = s.awarded[ci] || 0;\n return { name: c.name, hex: c.hex, deep: c.deep, revealed,\n rows: this.ROWS.map(label => ({ label })),\n models: c.models.map(([label, text]) => ({ label, text })),\n revealLabel: revealed ? 'Hide model' : 'Reveal model',\n reveal: () => this.setState(p => ({ revealed: { ...p.revealed, [ci]: !revealed } })),\n pts: [1, 2, 3, 4, 5].map(n => ({ n,\n bg: n <= cur ? 'var(--color-accent-100)' : 'transparent',\n border: n <= cur ? 'var(--color-accent)' : 'var(--color-divider)',\n color: n <= cur ? 'var(--color-accent-700)' : 'var(--color-neutral-600)',\n pick: () => this.setState(p => ({ awarded: { ...p.awarded, [ci]: (p.awarded[ci] === n ? 0 : n) } })) })) };\n });\n return { earned: (s.awarded[0] || 0) + (s.awarded[1] || 0) + (s.awarded[2] || 0),\n teacher: s.teacher, explain: s.explain,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplain: () => this.setState(p => ({ explain: !p.explain })),\n chars };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"11c0423f-20cc-4ca3-8d34-c57b7aaa6b0b": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "a4efa87f-68e2-432f-9d2d-530e5a5bd913": {"mime": "image/jpeg", "compressed": false, "data": "@@4346f924f586c6@@"}, "4152bde8-f90c-4c47-a6aa-6a6033b415eb": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "fae2b08f-4736-419b-aa75-2b7e83436994": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "9fa2b629-4d60-4511-b8db-6569e520b6ab": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "8396e29a-cbda-448c-bebe-33b63bdbfd72": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "12aeb3f9-6bfd-401f-bfd4-22c36c1150fa": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "b649d882-d196-4c88-a911-d9b8c780af1b": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "67c274db-ba5b-4cdf-b8d3-e0f1fc234379": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "7fae6f19-5719-494b-b2a4-d2bdf04d9617": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "bb52a460-a4b3-4e23-add8-fb5c3bfcc2cf": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "6f9bd52f-f4e3-4aff-934b-e52bc92c2d64": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "c9d0b658-38db-4a99-a0c1-47e0ad8a0492": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "e9f4d8ad-b67f-4a82-94ae-a48f12aca16c": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "ffde1acf-890e-483e-8235-b4e63b2bf6c1": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "05adace5-ccfb-4820-ba54-d4cd82e6de73": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "468d1525-b21d-4225-8be3-6b31c726547f": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "87306d58-0629-4802-939a-b0ff8b1026e8": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "7af547fd-41f4-474f-be58-d3162a791469": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "afd7b95f-f7bd-4bee-8609-84847c75cecb": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "73933856-14e0-48d4-b551-24f8b8b148a0": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "f6b1f11d-44ae-4c43-a504-86b93390667d": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "315fb355-6ee2-41fb-9b5f-0d6cfc2688a9": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "c3e3eb8c-291f-40d4-9f66-83d5b25d1c4a": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "96b60faf-5ebe-4de8-90f7-959c70377685": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "a631017b-8d13-43d9-b53b-65da6befc0d9": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f03c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f03c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f03c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f03c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"fae2b08f-4736-419b-aa75-2b7e83436994\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"9fa2b629-4d60-4511-b8db-6569e520b6ab\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8396e29a-cbda-448c-bebe-33b63bdbfd72\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"12aeb3f9-6bfd-401f-bfd4-22c36c1150fa\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b649d882-d196-4c88-a911-d9b8c780af1b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"67c274db-ba5b-4cdf-b8d3-e0f1fc234379\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"9fa2b629-4d60-4511-b8db-6569e520b6ab\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8396e29a-cbda-448c-bebe-33b63bdbfd72\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"12aeb3f9-6bfd-401f-bfd4-22c36c1150fa\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b649d882-d196-4c88-a911-d9b8c780af1b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"67c274db-ba5b-4cdf-b8d3-e0f1fc234379\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7fae6f19-5719-494b-b2a4-d2bdf04d9617\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"bb52a460-a4b3-4e23-add8-fb5c3bfcc2cf\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6f9bd52f-f4e3-4aff-934b-e52bc92c2d64\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c9d0b658-38db-4a99-a0c1-47e0ad8a0492\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e9f4d8ad-b67f-4a82-94ae-a48f12aca16c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ffde1acf-890e-483e-8235-b4e63b2bf6c1\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"05adace5-ccfb-4820-ba54-d4cd82e6de73\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7fae6f19-5719-494b-b2a4-d2bdf04d9617\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"bb52a460-a4b3-4e23-add8-fb5c3bfcc2cf\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6f9bd52f-f4e3-4aff-934b-e52bc92c2d64\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c9d0b658-38db-4a99-a0c1-47e0ad8a0492\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e9f4d8ad-b67f-4a82-94ae-a48f12aca16c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ffde1acf-890e-483e-8235-b4e63b2bf6c1\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"05adace5-ccfb-4820-ba54-d4cd82e6de73\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"468d1525-b21d-4225-8be3-6b31c726547f\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"87306d58-0629-4802-939a-b0ff8b1026e8\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7af547fd-41f4-474f-be58-d3162a791469\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"afd7b95f-f7bd-4bee-8609-84847c75cecb\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"73933856-14e0-48d4-b551-24f8b8b148a0\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f6b1f11d-44ae-4c43-a504-86b93390667d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"315fb355-6ee2-41fb-9b5f-0d6cfc2688a9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"468d1525-b21d-4225-8be3-6b31c726547f\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"87306d58-0629-4802-939a-b0ff8b1026e8\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7af547fd-41f4-474f-be58-d3162a791469\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"afd7b95f-f7bd-4bee-8609-84847c75cecb\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"73933856-14e0-48d4-b551-24f8b8b148a0\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f6b1f11d-44ae-4c43-a504-86b93390667d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"315fb355-6ee2-41fb-9b5f-0d6cfc2688a9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"c3e3eb8c-291f-40d4-9f66-83d5b25d1c4a\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"96b60faf-5ebe-4de8-90f7-959c70377685\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"a631017b-8d13-43d9-b53b-65da6befc0d9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"4152bde8-f90c-4c47-a6aa-6a6033b415eb\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 4 · Reading — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Two Cold Doors — Author’s Purpose<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"11c0423f-20cc-4ca3-8d34-c57b7aaa6b0b\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 8<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: read ¶1–5 and ¶19–32.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0;\">Students who say the fish story is “just a break” or has nothing to do with the plot. Send them back to ¶19: Dad says <em>“So, you see…”<\u002Fem> and then applies it directly. 2 points per row: 1 for the true idea, 1 for the paragraph number.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Afraid → what she decides is TRUE<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">6 points · 1 per idea, 1 per ¶<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Each fear gets answered by the chapter itself — that is the author’s purpose. Look at the example row first: Dad’s joke about the freezing door plants the idea. Then every fear meets a fact: she chooses to practise anyway, the tiger relied on courage (not luck), and the friends never changed — she did.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); font-size: 19px; color: var(--color-neutral-700); background: var(--color-neutral-100);\">\n <em>Example:<\u002Fem> The cold door outside will freeze her nose. → Dad is only joking — but his joke makes her think about the other cold door. <span style=\"font-variant-numeric: tabular-nums;\">¶3–4<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ rows }}\" as=\"r\" hint-placeholder-count=\"3\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ r.n }}.<\u002Fspan><em>What Pacy is afraid of:<\u002Fem> {{ r.fear }}<\u002Fdiv>\n <textarea rows=\"2\" placeholder=\"What does she decide is TRUE? And which ¶ proves it?\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ r.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ r.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ r.award }}\" style=\"background: {{ r.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ r.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ r.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px; line-height: 1.65;\"><em style=\"color: var(--color-accent-700);\">Model:<\u002Fem> {{ r.model }} <span style=\"font-variant-numeric: tabular-nums; color: #263d5c;\">{{ r.para }}<\u002Fspan><\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Think and write<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">2 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainB }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainB }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">The philosophers’ argument is the chapter’s answer in miniature: nobody outside you can know what you know, so nobody else can decide who you are. Placing it in the middle lets Dad make his point through a story instead of a lecture — it is the hinge the second half turns on.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\">Grace Lin puts the little story about the two philosophers and the fish right in the middle of the chapter. Why do you think she does that? What does it help Pacy understand?<\u002Fdiv>\n <textarea rows=\"3\" placeholder=\"Type the class answer here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ twReveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ twRevealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ twAward }}\" style=\"background: {{ twAwardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ twAwardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ twRevealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px; line-height: 1.65;\"><em style=\"color: var(--color-accent-700);\">Look for:<\u002Fem> nobody outside you can know what you know — so nobody else can decide who you are. The story lets Dad teach without lecturing, and it frees Pacy to choose for herself.<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"a4efa87f-68e2-432f-9d2d-530e5a5bd913\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 170px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { revealed: {}, awarded: {}, twRevealed: false, twAwarded: false, teacher: false, explainA: false, explainB: false };\n ROWS = [\n ['Being an author and illustrator is a \\u201ccold door\\u201d \\u2014 too hard, and it leads to a hard life.',\n 'She decides she will do it anyway, and practises on purpose: \\u201cIf I want to be an author and illustrator, I better practice as much as I can\\u201d / \\u201cno matter what.\\u201d', '\\u00b629\\u201332'],\n ['She needs tiger luck to be brave enough.',\n 'She does not need tiger luck \\u2014 she IS a tiger. The tiger in Dad\\u2019s story crossed by relying on his own courage and strength.', '\\u00b622\\u201324'],\n ['Becky and Charlotte changed, and they stopped liking her.',\n 'They had not changed \\u2014 she had. She had ignored them after Melody arrived.', '\\u00b635\\u201337'],\n ];\n renderVals() {\n const s = this.state;\n const rows = this.ROWS.map(([fear, model, para], i) => {\n const revealed = !!s.revealed[i], awarded = !!s.awarded[i];\n return { n: i + 1, fear, model, para, revealed,\n revealLabel: revealed ? 'Hide model' : 'Reveal model',\n awardLabel: awarded ? '\\u2713 2 points awarded' : 'Award 2 points',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ revealed: { ...p.revealed, [i]: !revealed } })),\n award: () => this.setState(p => ({ awarded: { ...p.awarded, [i]: !awarded } })) };\n });\n const earned = this.ROWS.reduce((a, _, i) => a + (s.awarded[i] ? 2 : 0), 0) + (s.twAwarded ? 2 : 0);\n return { earned, teacher: s.teacher, explainA: s.explainA, explainB: s.explainB,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n toggleExplainB: () => this.setState(p => ({ explainB: !p.explainB })),\n rows,\n twRevealed: s.twRevealed, twRevealLabel: s.twRevealed ? 'Hide the answer' : 'What to look for',\n twAwardLabel: s.twAwarded ? '\\u2713 2 points awarded' : 'Award 2 points',\n twAwardBg: s.twAwarded ? 'var(--color-accent-100)' : 'transparent',\n twReveal: () => this.setState(p => ({ twRevealed: !p.twRevealed })),\n twAward: () => this.setState(p => ({ twAwarded: !p.twAwarded })) };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"34ee6dfd-05f0-4b7d-934f-128e384188e8": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "a7418d2f-3548-49e6-92e4-0e69ebb6e1c4": {"mime": "image/jpeg", "compressed": false, "data": "@@c9e3157db338b3@@"}, "30519487-b471-4f89-8f81-a811643f518e": {"mime": "image/png", "compressed": false, "data": "@@037604d1ae2fda@@"}, "6b9df27d-9d28-4493-849f-a152f8d6d3b5": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "7ccab2bb-1c12-4530-8ada-2f1967ef85c1": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "2f6202cc-bd0e-4a74-bafe-d32d32af5460": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "64323860-2117-45eb-aa17-498e6ac8d7a7": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "110b65c3-75a8-41aa-aff5-46f1f97c43da": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "2a04f143-2258-49f3-bc93-4458410f88cc": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "ce6b1943-460c-4697-b51a-f4a5fb016751": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "71c173e4-bafc-41ca-9998-75a2df83c14d": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "43204926-5839-4ba6-a6ae-6df47712ba04": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "64e054a2-0b8b-4bd6-9b48-0671b458c5d2": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "42c8952d-8fa2-4c52-b2ff-1796bb59e56e": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "2158d549-1642-485c-a49e-a33dd5ac49b1": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "16426e73-7195-4316-a209-99bb0e59815e": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "880b095b-9b44-43b6-bb10-d3c2cc1c015c": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "71953da5-4597-4c69-871f-acfea5f2d3e5": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "06105a23-c8f4-45cf-b292-463b89ddcd7f": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "70030547-b5ad-47c2-812a-51e63cce62be": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "dad66bd1-c69f-4da0-a576-a14b52304b8b": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "ef6c0283-acd2-4c75-9112-65229b35117c": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "691230be-cbae-4baf-a680-488bc0a19839": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "4612b928-3c00-4219-8ca6-531afa5c27c5": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "c58d0c4c-d0bf-41da-8acb-570740c9e193": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "00484f05-31d5-462a-aa82-3fbe503441a6": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "e8b0c6f9-83e3-41d8-834f-5f1ad9680bc4": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f04c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f04c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f04c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f04c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"7ccab2bb-1c12-4530-8ada-2f1967ef85c1\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2f6202cc-bd0e-4a74-bafe-d32d32af5460\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"64323860-2117-45eb-aa17-498e6ac8d7a7\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"110b65c3-75a8-41aa-aff5-46f1f97c43da\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2a04f143-2258-49f3-bc93-4458410f88cc\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ce6b1943-460c-4697-b51a-f4a5fb016751\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2f6202cc-bd0e-4a74-bafe-d32d32af5460\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"64323860-2117-45eb-aa17-498e6ac8d7a7\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"110b65c3-75a8-41aa-aff5-46f1f97c43da\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2a04f143-2258-49f3-bc93-4458410f88cc\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ce6b1943-460c-4697-b51a-f4a5fb016751\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"71c173e4-bafc-41ca-9998-75a2df83c14d\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"43204926-5839-4ba6-a6ae-6df47712ba04\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"64e054a2-0b8b-4bd6-9b48-0671b458c5d2\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"42c8952d-8fa2-4c52-b2ff-1796bb59e56e\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2158d549-1642-485c-a49e-a33dd5ac49b1\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"16426e73-7195-4316-a209-99bb0e59815e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"880b095b-9b44-43b6-bb10-d3c2cc1c015c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"71c173e4-bafc-41ca-9998-75a2df83c14d\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"43204926-5839-4ba6-a6ae-6df47712ba04\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"64e054a2-0b8b-4bd6-9b48-0671b458c5d2\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"42c8952d-8fa2-4c52-b2ff-1796bb59e56e\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2158d549-1642-485c-a49e-a33dd5ac49b1\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"16426e73-7195-4316-a209-99bb0e59815e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"880b095b-9b44-43b6-bb10-d3c2cc1c015c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"71953da5-4597-4c69-871f-acfea5f2d3e5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"06105a23-c8f4-45cf-b292-463b89ddcd7f\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"70030547-b5ad-47c2-812a-51e63cce62be\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"dad66bd1-c69f-4da0-a576-a14b52304b8b\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ef6c0283-acd2-4c75-9112-65229b35117c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"691230be-cbae-4baf-a680-488bc0a19839\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4612b928-3c00-4219-8ca6-531afa5c27c5\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"71953da5-4597-4c69-871f-acfea5f2d3e5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"06105a23-c8f4-45cf-b292-463b89ddcd7f\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"70030547-b5ad-47c2-812a-51e63cce62be\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"dad66bd1-c69f-4da0-a576-a14b52304b8b\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ef6c0283-acd2-4c75-9112-65229b35117c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"691230be-cbae-4baf-a680-488bc0a19839\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4612b928-3c00-4219-8ca6-531afa5c27c5\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"c58d0c4c-d0bf-41da-8acb-570740c9e193\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"00484f05-31d5-462a-aa82-3fbe503441a6\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"e8b0c6f9-83e3-41d8-834f-5f1ad9680bc4\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"6b9df27d-9d28-4493-849f-a152f8d6d3b5\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 5 · Reading — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Point of View<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"34ee6dfd-05f0-4b7d-934f-128e384188e8\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-6); font-size: 19px;\">Before you start: any part of the story will do.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-2);\">In Part C the sentence must <em>show<\u002Fem> the meaning, not merely contain the word. “I have doubts” scores 0; “I had doubts about the test because I did not study” scores 1.<\u002Fp>\n <p style=\"margin: 0;\">Extension for early finishers: rewrite ¶2–3 in third person. Read both aloud. What do we lose when we cannot hear Pacy think?<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Match each term to its meaning<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">4 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Two of these are <em>people-words<\u002Fem> and two are <em>camera-words<\u002Fem>. The <em>narrator<\u002Fem> is a who — the person telling the story. <em>Point of view<\u002Fem> is the camera position: who is telling it, and how much they can know. <em>First-person<\u002Fem> puts the camera inside a character (I, me); <em>third-person<\u002Fem> puts it outside (he, she). Sort people-words first, then camera-words.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Tap a term, then tap its meaning. A correct pair locks in.<\u002Fp>\n <div style=\"display: grid; grid-template-columns: minmax(240px, 320px) 1fr; gap: var(--space-8); align-items: start;\">\n <div style=\"display: flex; flex-direction: column; gap: var(--space-3);\">\n <sc-for list=\"{{ words }}\" as=\"w\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ w.pick }}\" style=\"text-align: left; font-family: var(--font-body); font-size: 30px; font-weight: 600; padding: 12px 18px; min-height: 52px; border-radius: var(--radius-md); cursor: pointer; background: {{ w.bg }}; border: {{ w.border }}; color: {{ w.color }};\">\n <span style=\"font-variant-numeric: tabular-nums; font-size: 19px; color: var(--color-neutral-600); font-family: var(--font-body); margin-right: 10px;\">{{ w.n }}<\u002Fspan>{{ w.text }}<span style=\"color: var(--color-accent-700);\">{{ w.tick }}<\u002Fspan>\n <\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-3);\">\n <sc-for list=\"{{ meanings }}\" as=\"m\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ m.pick }}\" style=\"width: 100%; text-align: left; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: 12px 18px; min-height: 52px; border-radius: var(--radius-md); cursor: pointer; display: flex; align-items: center; gap: var(--space-4); background: {{ m.bg }}; border: {{ m.border }}; animation: {{ m.anim }};\">\n <span style=\"font-family: var(--font-heading); font-style: italic; font-size: 22px; color: var(--color-neutral-600); width: 16px;\">{{ m.letter }}<\u002Fspan>\n <span style=\"flex: 1;\">{{ m.text }}<\u002Fspan>\n <span style=\"font-family: var(--font-body); font-weight: 600; font-size: 24px; color: var(--color-accent-700); white-space: nowrap;\">{{ m.badge }}<\u002Fspan>\n <\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Look at the story and answer<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">4 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainB }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainB }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">The trap in question 1 is <em>Grace Lin<\u002Fem>: she is the <em>author<\u002Fem> — she wrote the book — but the <em>narrator<\u002Fem> is the voice inside it, and that voice is Pacy’s. The proof is the pronouns: <em>I, my, me<\u002Fem>. And that is exactly why question 4 works — a first-person narrator lets us hear Pacy think; hand the story to Becky and Pacy’s thoughts go silent.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: grid; grid-template-columns: 1fr 250px; gap: var(--space-6); align-items: start;\">\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); animation: {{ q1Anim }};\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">1.<\u002Fspan>Who tells <em>The Year of the Rat<\u002Fem>?<\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-3); flex-wrap: wrap;\">\n <sc-for list=\"{{ q1Opts }}\" as=\"o\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ o.pick }}\" style=\"min-height: 48px; padding: 8px 24px; font-family: var(--font-body); font-size: 26px; font-weight: 600; border: {{ o.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ o.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ o.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ q1Hint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-3);\">Grace Lin is the author — she wrote it. Who is the voice telling it, saying “I”?<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); animation: {{ q2Anim }};\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">2.<\u002Fspan>Tap two words from this line of ¶2–5 that show it: <em>“I put on my coat and Dad opened the door for me.”<\u002Fem><\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-2); flex-wrap: wrap;\">\n <sc-for list=\"{{ q2Words }}\" as=\"w\" hint-placeholder-count=\"11\">\n <button sc-camel-on-click=\"{{ w.pick }}\" style=\"min-height: 46px; padding: 6px 16px; font-family: var(--font-body); font-size: 24px; border: {{ w.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ w.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ w.text }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ q2Hint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-3);\">Look for the little words that mean the teller herself — I, my, me.<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">3.<\u002Fspan>Rewrite this line in third person: <em>“I closed the door.”<\u002Fem><\u002Fdiv>\n <textarea rows=\"1\" placeholder=\"Type the class answer here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ q3Reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ q3RevealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ q3Award }}\" style=\"background: {{ q3AwardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ q3AwardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ q3Revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px;\"><em style=\"color: var(--color-accent-700);\">Model:<\u002Fem> She closed the door.<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); animation: {{ q4Anim }};\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">4.<\u002Fspan>If Becky told this story instead of Pacy, we would —<\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-3); flex-wrap: wrap;\">\n <sc-for list=\"{{ q4Opts }}\" as=\"o\" hint-placeholder-count=\"2\">\n <button sc-camel-on-click=\"{{ o.pick }}\" style=\"min-height: 48px; padding: 8px 24px; font-family: var(--font-body); font-size: 24px; border: {{ o.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ o.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ o.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ q4Hint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-3);\">Becky cannot hear Pacy think. Whose head would the camera be in?<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fdiv>\n <figure class=\"plate\" style=\"margin: 0;\"><img src=\"a7418d2f-3548-49e6-92e4-0e69ebb6e1c4\" alt=\"Pacy and Dad\" style=\"width: 100%; display: block;\"><\u002Ffigure>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">C<\u002Fspan>Critical vocabulary — your own sentence<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">2 points<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">The sentence must <em>show<\u002Fem> the meaning, not just contain the word. Build one together, then award the point.<\u002Fp>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ cItems }}\" as=\"c\" hint-placeholder-count=\"2\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><strong style=\"font-family: var(--font-body); font-size: 22px;\">{{ c.word }}<\u002Fstrong> — {{ c.def }}<\u002Fdiv>\n <textarea rows=\"2\" placeholder=\"Type the class sentence here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ c.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ c.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ c.award }}\" style=\"background: {{ c.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ c.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ c.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px; line-height: 1.65;\"><em style=\"color: var(--color-accent-700);\">Test it:<\u002Fem> {{ c.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"30519487-b471-4f89-8f81-a811643f518e\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { sel: null, matched: {}, tries: {}, shakeM: null, q1: false, q1Tries: 0, shakeQ1: false, q2Got: {}, q2Tries: 0, q2Done: false, shakeQ2: false, q3Revealed: false, q3Awarded: false, q4: false, q4Tries: 0, shakeQ4: false, cRevealed: {}, cAwarded: {}, earned: 0, teacher: false, explainA: false, explainB: false };\n PAIRS = { 'point of view': 'c', 'narrator': 'a', 'first-person': 'b', 'third-person': 'd' };\n MEANINGS = [\n ['a', 'the person who tells the story'],\n ['b', 'the story is told by a character in it, using I and me'],\n ['c', 'who is telling the story, and how much that person knows'],\n ['d', 'the story is told from outside, using he and she'],\n ];\n Q2 = ['I', 'put', 'on', 'my', 'coat', 'and', 'Dad', 'opened', 'the', 'door', 'me'];\n Q2GOOD = { I: 1, my: 1, me: 1 };\n shake(key) { this.setState({ [key]: true }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: false }), 450); }\n shakeId(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const wordNames = Object.keys(this.PAIRS);\n const byLetter = {}; wordNames.forEach(w => byLetter[this.PAIRS[w]] = w);\n const words = wordNames.map((w, i) => {\n const m = !!s.matched[this.PAIRS[w]], sel = s.sel === w;\n return { n: i + 1, text: w, tick: m ? ' \\u2713' : '',\n bg: sel ? 'var(--color-accent-100)' : 'transparent',\n border: sel ? '1px solid var(--color-accent)' : m ? '1px solid var(--color-accent-300)' : '1px solid var(--color-divider)',\n color: m ? 'var(--color-neutral-600)' : 'var(--color-text)',\n pick: () => { if (!m) this.setState({ sel: sel ? null : w }); } };\n });\n const meanings = this.MEANINGS.map(([letter, text]) => {\n const m = !!s.matched[letter];\n return { letter, text, badge: m ? byLetter[letter] : '',\n bg: m ? 'var(--color-accent-100)' : 'transparent',\n border: m ? '1px solid var(--color-accent-300)' : '1px solid var(--color-divider)',\n anim: s.shakeM === letter ? 'omShake 0.4s' : 'none',\n pick: () => {\n if (m || !s.sel) return;\n if (this.PAIRS[s.sel] === letter) this.setState(p => ({ matched: { ...p.matched, [letter]: true }, sel: null, earned: p.earned + 1 }));\n else { this.setState(p => ({ tries: { ...p.tries, [letter]: (p.tries[letter] || 0) + 1 } })); this.shakeId('shakeM', letter); }\n } };\n });\n const mkOpt = (label, ok, doneKey, triesKey, shakeKey) => ({ label,\n bg: s[doneKey] && ok ? 'var(--color-accent-100)' : 'transparent',\n border: s[doneKey] && ok ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (s[doneKey]) return;\n if (ok) this.setState(p => ({ [doneKey]: true, earned: p.earned + 1 }));\n else { this.setState(p => ({ [triesKey]: p[triesKey] + 1 })); this.shake(shakeKey); }\n } });\n const q1Opts = [['Pacy', true], ['Grace Lin', false], ['Dad', false], ['Becky', false]].map(([l, ok]) => mkOpt(l, ok, 'q1', 'q1Tries', 'shakeQ1'));\n const q4Opts = [['know more about how Pacy feels', false], ['know less about how Pacy feels', true]].map(([l, ok]) => mkOpt(l, ok, 'q4', 'q4Tries', 'shakeQ4'));\n const q2Words = this.Q2.map((text, i) => {\n const got = !!s.q2Got[i];\n return { text,\n bg: got ? 'var(--color-accent-100)' : 'transparent',\n border: got ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (got || s.q2Done) return;\n if (this.Q2GOOD[text]) this.setState(p => {\n const q2Got = { ...p.q2Got, [i]: true };\n const n = Object.keys(q2Got).length;\n return { q2Got, q2Done: n >= 2, earned: n === 2 ? p.earned + 1 : p.earned };\n });\n else { this.setState(p => ({ q2Tries: p.q2Tries + 1 })); this.shake('shakeQ2'); }\n } };\n });\n const C = [\n ['doubts', 'the feeling of not being sure about something', 'Does the sentence give a reason or a situation? \\u201cI have doubts\\u201d shows nothing; \\u201cI had doubts about the test because I did not study\\u201d shows the meaning.'],\n ['relying', 'trusting or depending on someone', 'Who is depending on whom, and for what? \\u201cThe baby birds are relying on their mother for food\\u201d shows it.'],\n ];\n const cItems = C.map(([word, def, model], i) => {\n const revealed = !!s.cRevealed[i], awarded = !!s.cAwarded[i];\n return { word, def, model, revealed,\n revealLabel: revealed ? 'Hide the test' : 'How do we test it?',\n awardLabel: awarded ? '\\u2713 Point awarded' : 'Award point',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ cRevealed: { ...p.cRevealed, [i]: !revealed } })),\n award: () => this.setState(p => ({ cAwarded: { ...p.cAwarded, [i]: !awarded }, earned: p.earned + (awarded ? -1 : 1) })) };\n });\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA, explainB: s.explainB,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n toggleExplainB: () => this.setState(p => ({ explainB: !p.explainB })),\n words, meanings, q1Opts, q4Opts, q2Words,\n q1Anim: s.shakeQ1 ? 'omShake 0.4s' : 'none', q2Anim: s.shakeQ2 ? 'omShake 0.4s' : 'none', q4Anim: s.shakeQ4 ? 'omShake 0.4s' : 'none',\n q1Hint: !s.q1 && s.q1Tries >= 2, q2Hint: !s.q2Done && s.q2Tries >= 2, q4Hint: !s.q4 && s.q4Tries >= 2,\n q3Revealed: s.q3Revealed, q3RevealLabel: s.q3Revealed ? 'Hide model' : 'Reveal model',\n q3AwardLabel: s.q3Awarded ? '\\u2713 Point awarded' : 'Award point',\n q3AwardBg: s.q3Awarded ? 'var(--color-accent-100)' : 'transparent',\n q3Reveal: () => this.setState(p => ({ q3Revealed: !p.q3Revealed })),\n q3Award: () => this.setState(p => ({ q3Awarded: !p.q3Awarded, earned: p.earned + (p.q3Awarded ? -1 : 1) })),\n cItems };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"9e41a0c6-6162-4ced-9277-dd54778cab29": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "6e2aa589-7642-4c3a-822f-a077d11fceb0": {"mime": "image/png", "compressed": false, "data": "@@037604d1ae2fda@@"}, "a332538c-8fe6-4336-90af-f51afeff5141": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "a5d86471-f4f4-40c3-b5df-733b29aaafe0": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "ad2147a6-ff28-49c7-a420-45c9fd1c8d70": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "e8db2bd6-9d80-4981-bf6c-63997779378f": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "40ae5486-dde4-449e-95fe-58d2e6a20abf": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "6374dd90-8bb7-4c9b-b8d3-d052155d73fd": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "3da78ffa-9efb-42fb-8ff7-e1060319512b": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "b497e28e-59b5-4547-b34a-064dfeea5e2f": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "48b99545-b180-44bc-8a12-68dad1df391b": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "6980a5c3-2498-48cc-b191-8cff5f4a5268": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "92a6f710-9c49-48ef-aa8d-c95afa1f9f11": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "21d09a08-c0af-417d-b57e-e737e0c33ee0": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "bf10fb82-890f-4795-be72-d84060c26558": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "73ce8f7e-8b21-4a87-b33f-4c06c492f793": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "7963b1bc-6385-4338-9023-f3a37eebb788": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "b761a60f-dd78-4b86-9f37-0fb251472114": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "29b88486-970d-41f9-b5b8-0294c3644079": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "99f54432-85da-415d-be1a-efec8ce1b985": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "5b1b45c0-c34f-4723-93a2-d6451525cf05": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "a587bf46-be3f-4a32-8f7c-6ca3d1c7d3ac": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "a913462b-c38e-441f-83d8-27ea88a8d6ee": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "9ba138ab-c34e-40f8-9513-3da36e2d430a": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "5a326fb7-fcdd-462a-ab53-74be9cb15586": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "9e0a869c-299c-4ab1-a39a-dfdb39fb910e": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f05c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f05c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f05c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f05c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"a5d86471-f4f4-40c3-b5df-733b29aaafe0\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ad2147a6-ff28-49c7-a420-45c9fd1c8d70\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e8db2bd6-9d80-4981-bf6c-63997779378f\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"40ae5486-dde4-449e-95fe-58d2e6a20abf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6374dd90-8bb7-4c9b-b8d3-d052155d73fd\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3da78ffa-9efb-42fb-8ff7-e1060319512b\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ad2147a6-ff28-49c7-a420-45c9fd1c8d70\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e8db2bd6-9d80-4981-bf6c-63997779378f\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"40ae5486-dde4-449e-95fe-58d2e6a20abf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6374dd90-8bb7-4c9b-b8d3-d052155d73fd\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3da78ffa-9efb-42fb-8ff7-e1060319512b\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b497e28e-59b5-4547-b34a-064dfeea5e2f\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"48b99545-b180-44bc-8a12-68dad1df391b\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6980a5c3-2498-48cc-b191-8cff5f4a5268\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"92a6f710-9c49-48ef-aa8d-c95afa1f9f11\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"21d09a08-c0af-417d-b57e-e737e0c33ee0\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"bf10fb82-890f-4795-be72-d84060c26558\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"73ce8f7e-8b21-4a87-b33f-4c06c492f793\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b497e28e-59b5-4547-b34a-064dfeea5e2f\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"48b99545-b180-44bc-8a12-68dad1df391b\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6980a5c3-2498-48cc-b191-8cff5f4a5268\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"92a6f710-9c49-48ef-aa8d-c95afa1f9f11\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"21d09a08-c0af-417d-b57e-e737e0c33ee0\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"bf10fb82-890f-4795-be72-d84060c26558\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"73ce8f7e-8b21-4a87-b33f-4c06c492f793\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7963b1bc-6385-4338-9023-f3a37eebb788\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b761a60f-dd78-4b86-9f37-0fb251472114\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"29b88486-970d-41f9-b5b8-0294c3644079\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"99f54432-85da-415d-be1a-efec8ce1b985\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5b1b45c0-c34f-4723-93a2-d6451525cf05\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a587bf46-be3f-4a32-8f7c-6ca3d1c7d3ac\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a913462b-c38e-441f-83d8-27ea88a8d6ee\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7963b1bc-6385-4338-9023-f3a37eebb788\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b761a60f-dd78-4b86-9f37-0fb251472114\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"29b88486-970d-41f9-b5b8-0294c3644079\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"99f54432-85da-415d-be1a-efec8ce1b985\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5b1b45c0-c34f-4723-93a2-d6451525cf05\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a587bf46-be3f-4a32-8f7c-6ca3d1c7d3ac\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a913462b-c38e-441f-83d8-27ea88a8d6ee\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"9ba138ab-c34e-40f8-9513-3da36e2d430a\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5a326fb7-fcdd-462a-ab53-74be9cb15586\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"9e0a869c-299c-4ab1-a39a-dfdb39fb910e\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"a332538c-8fe6-4336-90af-f51afeff5141\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 6 · Reading — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Show What You Know<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"9e41a0c6-6162-4ced-9277-dd54778cab29\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: read the whole story. Answer in a full sentence, then prove it with the paragraph number.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0;\">1 pt for a full-sentence answer + 1 pt for a sensible ¶ citation, per question. Paragraph numbers may shift slightly between printings — accept anything in range.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">?<\u002Fspan>Five questions, five proofs<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">10 points · 1 answer + 1 ¶ each<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplain }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explain }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">A full sentence answers the question on its own — someone who never read the question should still understand it. And every answer needs its proof: <em>which paragraph shows it?<\u002Fem> If the class cannot name a ¶, the answer is a guess, not a find.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ rows }}\" as=\"r\" hint-placeholder-count=\"5\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ r.n }}.<\u002Fspan>{{ r.q }}<\u002Fdiv>\n <textarea rows=\"2\" placeholder=\"Full sentence + ¶ number…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ r.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ r.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ r.award }}\" style=\"background: {{ r.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ r.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ r.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px; line-height: 1.65;\"><em style=\"color: var(--color-accent-700);\">Model:<\u002Fem> {{ r.model }} <span style=\"font-variant-numeric: tabular-nums; color: #263d5c;\">{{ r.para }}<\u002Fspan><\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"6e2aa589-7642-4c3a-822f-a077d11fceb0\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { revealed: {}, awarded: {}, teacher: false, explain: false };\n ROWS = [\n ['What are the two cold doors in this story?',\n 'The freezing door of the house on the snow day, and the \\u201ccold door\\u201d of becoming an author and illustrator.', '\\u00b62\\u20134'],\n ['Why does Dad tell Pacy the story about the two philosophers?',\n 'To show her that only she can know herself \\u2014 one philosopher cannot know what the other knows, so nobody can decide for Pacy who she is.', '\\u00b614\\u201319'],\n ['Pacy was born in the year of the tiger. Why does that matter to her by the end?',\n 'The tiger crossed the wild water relying on his own courage, not luck. She decides she does not need tiger luck because she IS a tiger \\u2014 and at the end she hears a tiger roar inside her.', '\\u00b622\\u201324, \\u00b646'],\n ['Why did Pacy stop being such good friends with Becky and Charlotte?',\n 'When Melody moved to New Hartford two years ago they became best friends, and Pacy ignored Becky and Charlotte. She had changed; they had not.', '\\u00b635\\u201337'],\n ['What does Pacy do when Becky calls Dun-Wei \\u201cDumb-Way\\u201d?',\n 'She takes a deep breath, gathers her tiger strength and tells Becky it is not nice \\u2014 \\u201cHis name is Dun-Wei.\\u201d Becky agrees and stops.', '\\u00b638\\u201342'],\n ];\n renderVals() {\n const s = this.state;\n const rows = this.ROWS.map(([q, model, para], i) => {\n const revealed = !!s.revealed[i], awarded = !!s.awarded[i];\n return { n: i + 1, q, model, para, revealed,\n revealLabel: revealed ? 'Hide model' : 'Reveal model',\n awardLabel: awarded ? '\\u2713 2 points awarded' : 'Award 2 points',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ revealed: { ...p.revealed, [i]: !revealed } })),\n award: () => this.setState(p => ({ awarded: { ...p.awarded, [i]: !awarded } })) };\n });\n return { earned: this.ROWS.reduce((a, _, i) => a + (s.awarded[i] ? 2 : 0), 0),\n teacher: s.teacher, explain: s.explain,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplain: () => this.setState(p => ({ explain: !p.explain })),\n rows };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"6759a958-8ca0-4d3c-a10e-0892dbd0ad11": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "1fbbc8fe-c5aa-4f1f-8379-7fc1eafb870d": {"mime": "image/png", "compressed": false, "data": "@@f67d5a848c0ea2@@"}, "b93101ec-8a88-4cb7-91be-6e100a0909d2": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "f799023c-6878-4cbb-840f-951b8b5f607a": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "51b352a2-793a-4ddb-aad9-79a297bfc5a3": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "b206b234-d072-49f5-8253-a0c7a8e5e516": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "669afb51-0e90-4822-a5a7-2cc78c2c2b7b": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "3eb5b2ed-157c-4298-a4a2-3aad8611dada": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "84e0a7b0-0eb9-49dd-ad29-e42f7c45134c": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "b116ce48-ee51-4c61-ae2a-ba7a34cda6dd": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "cf9cb87f-8b34-4098-b795-f0d50d0f0887": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "0884eaaa-274e-4c0d-9f92-70fdbe83537d": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "7ef75fa4-130e-4a76-97b1-8704cfa79f81": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "e73e9e55-3524-4ca9-9eea-23104aa488a3": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "3e7c092a-04f1-4b90-b518-4fde7d364c85": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "dea656dd-e1cd-4913-93e2-33c15cb5788c": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "ce076d8b-98f0-4cbb-8ac7-ee0cd7f95a72": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "66f05c7a-fbe9-4629-baae-f785f670b52a": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "011cf311-a421-4484-8cd5-44cc6e44d7fc": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "d8122ec8-fdad-4a0e-8a77-b0e53093dbe3": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "a9102994-4ef5-46f6-bfd8-c957bd0ed42b": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "6a9b8945-0765-4fe4-87be-cf5a43908f4d": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "615e11bd-93a4-4df2-b1a2-eefe8249c016": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "05b730cc-a0c2-49c1-90c5-52a064bf3be9": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "898f130d-5572-406d-bf96-e351e0a12ad5": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "3bb3f100-890c-4a39-b7ae-1c9f571f8a28": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f06c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f06c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f06c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f06c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"f799023c-6878-4cbb-840f-951b8b5f607a\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"51b352a2-793a-4ddb-aad9-79a297bfc5a3\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b206b234-d072-49f5-8253-a0c7a8e5e516\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"669afb51-0e90-4822-a5a7-2cc78c2c2b7b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3eb5b2ed-157c-4298-a4a2-3aad8611dada\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"84e0a7b0-0eb9-49dd-ad29-e42f7c45134c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"51b352a2-793a-4ddb-aad9-79a297bfc5a3\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b206b234-d072-49f5-8253-a0c7a8e5e516\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"669afb51-0e90-4822-a5a7-2cc78c2c2b7b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3eb5b2ed-157c-4298-a4a2-3aad8611dada\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"84e0a7b0-0eb9-49dd-ad29-e42f7c45134c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b116ce48-ee51-4c61-ae2a-ba7a34cda6dd\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cf9cb87f-8b34-4098-b795-f0d50d0f0887\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0884eaaa-274e-4c0d-9f92-70fdbe83537d\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7ef75fa4-130e-4a76-97b1-8704cfa79f81\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e73e9e55-3524-4ca9-9eea-23104aa488a3\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3e7c092a-04f1-4b90-b518-4fde7d364c85\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"dea656dd-e1cd-4913-93e2-33c15cb5788c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b116ce48-ee51-4c61-ae2a-ba7a34cda6dd\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"cf9cb87f-8b34-4098-b795-f0d50d0f0887\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0884eaaa-274e-4c0d-9f92-70fdbe83537d\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7ef75fa4-130e-4a76-97b1-8704cfa79f81\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e73e9e55-3524-4ca9-9eea-23104aa488a3\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3e7c092a-04f1-4b90-b518-4fde7d364c85\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"dea656dd-e1cd-4913-93e2-33c15cb5788c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ce076d8b-98f0-4cbb-8ac7-ee0cd7f95a72\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"66f05c7a-fbe9-4629-baae-f785f670b52a\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"011cf311-a421-4484-8cd5-44cc6e44d7fc\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d8122ec8-fdad-4a0e-8a77-b0e53093dbe3\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a9102994-4ef5-46f6-bfd8-c957bd0ed42b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6a9b8945-0765-4fe4-87be-cf5a43908f4d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"615e11bd-93a4-4df2-b1a2-eefe8249c016\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ce076d8b-98f0-4cbb-8ac7-ee0cd7f95a72\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"66f05c7a-fbe9-4629-baae-f785f670b52a\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"011cf311-a421-4484-8cd5-44cc6e44d7fc\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d8122ec8-fdad-4a0e-8a77-b0e53093dbe3\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a9102994-4ef5-46f6-bfd8-c957bd0ed42b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6a9b8945-0765-4fe4-87be-cf5a43908f4d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"615e11bd-93a4-4df2-b1a2-eefe8249c016\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"05b730cc-a0c2-49c1-90c5-52a064bf3be9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"898f130d-5572-406d-bf96-e351e0a12ad5\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"3bb3f100-890c-4a39-b7ae-1c9f571f8a28\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"b93101ec-8a88-4cb7-91be-6e100a0909d2\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 7 · Reading — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Build a Paragraph<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"6759a958-8ca0-4d3c-a10e-0892dbd0ad11\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: read the whole story.<\u002Fp>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-6); font-size: 20px; line-height: 1.7; background: var(--color-neutral-100);\">\n <em>Example:<\u002Fem> <strong>Topic:<\u002Fstrong> Pacy decides she will be an author and illustrator. <strong>Supporting:<\u002Fstrong> Her dad tells her that only she can know herself. · She remembers the tiger who crossed the water by relying on himself. · She tells Charlotte she will practise no matter what. <strong>Concluding:<\u002Fstrong> By the end of the chapter, the cold door does not frighten her any more.\n <\u002Fdiv>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0;\">Common error: supporting sentences that are opinions (“I like Pacy”) instead of details from the text. Send the student back to the page for a paragraph number. Topic 2 pts · each supporting sentence 2 pts · concluding 2 pts.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">1<\u002Fspan>Tick ONE topic<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplain }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explain }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">A paragraph is a sandwich: the topic sentence says one clear idea, three supporting sentences each bring a <em>detail from the text<\u002Fem> (not an opinion), and the concluding sentence says the idea again in new words. Pick the topic the class has the most evidence for — then hunt details before writing.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-3);\">\n <sc-for list=\"{{ topics }}\" as=\"t\" hint-placeholder-count=\"3\">\n <button sc-camel-on-click=\"{{ t.pick }}\" style=\"width: 100%; text-align: left; display: flex; align-items: center; gap: var(--space-4); font-family: var(--font-body); font-size: 24px; line-height: 1.5; padding: 12px 18px; min-height: 54px; border-radius: var(--radius-md); cursor: pointer; background: {{ t.bg }}; border: {{ t.border }};\">\n <span style=\"width: 32px; height: 32px; flex: none; border: 1.5px solid {{ t.boxColor }}; border-radius: var(--radius-sm, 4px); display: inline-flex; align-items: center; justify-content: center; font-size: 26px; color: var(--color-accent-700);\">{{ t.tick }}<\u002Fspan>\n <span>{{ t.text }}<\u002Fspan>\n <\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">2<\u002Fspan>Write it together<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">10 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ hintsReveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ hintsLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ hintsOn }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Strong details to expect · {{ hintTopic }}<\u002Fdiv>\n <p style=\"margin: 0;\">{{ hintText }}<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ parts }}\" as=\"p\" hint-placeholder-count=\"5\">\n <div style=\"border: 1px solid var(--color-divider); border-left: 4px solid {{ p.hex }}; border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);\">\n <div style=\"display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2);\">\n <span style=\"font-size: 15px; letter-spacing: 0.12em; text-transform: uppercase; color: {{ p.deep }};\">{{ p.label }}<\u002Fspan>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <button sc-camel-on-click=\"{{ p.award }}\" style=\"background: {{ p.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 14px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ p.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <textarea rows=\"2\" placeholder=\"{{ p.ph }}\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"1fbbc8fe-c5aa-4f1f-8379-7fc1eafb870d\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { topic: null, awarded: {}, hintsOn: false, teacher: false, explain: false };\n TOPICS = [\n 'Why Pacy decides she can be an author and illustrator',\n 'Why Pacy tells Becky to stop saying \\u201cDumb-Way\\u201d',\n 'Why Pacy\\u2019s dad tells her the story about the two philosophers',\n ];\n HINTS = [\n 'Dad says only she can know herself \\u00b7 the tiger crossed by relying on himself \\u00b7 she chooses the panda project alone to get practice \\u00b7 \\u201cno matter what.\\u201d',\n 'She has changed and it bothers her now \\u00b7 she gathers tiger strength \\u00b7 she says it is mean \\u00b7 Becky listens and agrees.',\n 'Pacy asks a question Dad cannot answer directly \\u00b7 the story shows nobody can know another person\\u2019s mind \\u00b7 it hands the decision back to her.',\n ];\n PARTS = [\n ['Topic sentence', 'One clear idea\\u2026', '#33517a', '#263d5c'],\n ['Supporting sentence 1', 'A detail from the text\\u2026', '#b5372f', '#8e2a24'],\n ['Supporting sentence 2', 'Another detail\\u2026', '#b5372f', '#8e2a24'],\n ['Supporting sentence 3', 'And one more\\u2026', '#b5372f', '#8e2a24'],\n ['Concluding sentence', 'The idea again, in new words\\u2026', '#c9973f', '#7c5520'],\n ];\n renderVals() {\n const s = this.state;\n const topics = this.TOPICS.map((text, i) => {\n const on = s.topic === i;\n return { text, tick: on ? '\\u2713' : '\\u00a0',\n bg: on ? 'var(--color-accent-100)' : 'transparent',\n border: on ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n boxColor: on ? 'var(--color-accent)' : 'var(--color-neutral-400)',\n pick: () => this.setState({ topic: on ? null : i }) };\n });\n const parts = this.PARTS.map(([label, ph, hex, deep], i) => {\n const awarded = !!s.awarded[i];\n return { label, ph, hex, deep,\n awardLabel: awarded ? '\\u2713 2 pts' : 'Award 2 pts',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n award: () => this.setState(p => ({ awarded: { ...p.awarded, [i]: !awarded } })) };\n });\n return { earned: this.PARTS.reduce((a, _, i) => a + (s.awarded[i] ? 2 : 0), 0),\n teacher: s.teacher, explain: s.explain, topics, parts,\n hintsOn: s.hintsOn && s.topic != null,\n hintsLabel: s.hintsOn ? 'Hide expected details' : 'Details to expect',\n hintTopic: s.topic != null ? 'Topic ' + (s.topic + 1) : '',\n hintText: s.topic != null ? this.HINTS[s.topic] : 'Pick a topic first.',\n hintsReveal: () => this.setState(p => ({ hintsOn: !p.hintsOn })),\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplain: () => this.setState(p => ({ explain: !p.explain })) };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"37349370-347e-476c-be5a-4c3b75eaf666": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "dd4d1f1c-0a88-4bb5-8fd5-4d889a034b87": {"mime": "image/png", "compressed": false, "data": "@@84a0ed38bb4353@@"}, "6a73bf89-484a-4728-b180-a42a9be6b0ee": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "08b62837-f697-4159-81fb-db474f44c759": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "30c985bd-bdf2-400d-8143-49bab498d57f": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "094ff396-cca6-4174-87c8-b28b7c835077": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "76ab15fc-61f2-404f-95a9-8e647ac0c7ab": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "6a80934c-6a8c-4a94-97c8-da816168a5f9": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "078ab24f-7620-427d-b70f-da3316ff8df0": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "e576216d-d75f-4685-9182-224261699c21": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "51208905-5618-4dc3-8338-bd52d2fd5dc2": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "ac7c2b64-1c98-4c3d-a40b-2135294d889b": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "24493ab2-1d2d-4c40-ae3c-a48ac7467bbd": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "82efabae-10e4-4a3c-8d29-c1033490ab7e": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "35381569-ce10-4d98-9fec-2f4156cc8d41": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "e8343f3c-c850-45a4-8e7b-e289ca32d2f5": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "126fc856-1b05-4a3f-b0ca-f84e22cedc8b": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "3e249917-d42e-4eed-89de-e492c768cfd6": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "333f691f-a2fc-4ae4-8ccd-bb8ae6f6f61b": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "a14fb587-f942-4431-8907-a6d040ba5503": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "001874fc-347f-4b65-8f44-797de3e796ab": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "c14988e2-c0b0-43b9-8962-f6232d907c6b": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "fe5804f5-17cb-465b-8dcb-62e86ff26675": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "3b4ba021-b7fe-45ae-b5ce-f43f66f0280f": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "1ae6fd4c-2119-4d0c-b618-ada107e8657f": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "8ab1c3bc-d215-4e0c-9f8b-57f5b552e0a4": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f07c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f07c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f07c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f07c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"6a73bf89-484a-4728-b180-a42a9be6b0ee\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"30c985bd-bdf2-400d-8143-49bab498d57f\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"094ff396-cca6-4174-87c8-b28b7c835077\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"76ab15fc-61f2-404f-95a9-8e647ac0c7ab\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6a80934c-6a8c-4a94-97c8-da816168a5f9\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"078ab24f-7620-427d-b70f-da3316ff8df0\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"30c985bd-bdf2-400d-8143-49bab498d57f\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"094ff396-cca6-4174-87c8-b28b7c835077\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"76ab15fc-61f2-404f-95a9-8e647ac0c7ab\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6a80934c-6a8c-4a94-97c8-da816168a5f9\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"078ab24f-7620-427d-b70f-da3316ff8df0\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e576216d-d75f-4685-9182-224261699c21\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"51208905-5618-4dc3-8338-bd52d2fd5dc2\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ac7c2b64-1c98-4c3d-a40b-2135294d889b\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"24493ab2-1d2d-4c40-ae3c-a48ac7467bbd\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"82efabae-10e4-4a3c-8d29-c1033490ab7e\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"35381569-ce10-4d98-9fec-2f4156cc8d41\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e8343f3c-c850-45a4-8e7b-e289ca32d2f5\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e576216d-d75f-4685-9182-224261699c21\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"51208905-5618-4dc3-8338-bd52d2fd5dc2\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ac7c2b64-1c98-4c3d-a40b-2135294d889b\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"24493ab2-1d2d-4c40-ae3c-a48ac7467bbd\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"82efabae-10e4-4a3c-8d29-c1033490ab7e\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"35381569-ce10-4d98-9fec-2f4156cc8d41\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e8343f3c-c850-45a4-8e7b-e289ca32d2f5\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"126fc856-1b05-4a3f-b0ca-f84e22cedc8b\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3e249917-d42e-4eed-89de-e492c768cfd6\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"333f691f-a2fc-4ae4-8ccd-bb8ae6f6f61b\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a14fb587-f942-4431-8907-a6d040ba5503\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"001874fc-347f-4b65-8f44-797de3e796ab\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c14988e2-c0b0-43b9-8962-f6232d907c6b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"fe5804f5-17cb-465b-8dcb-62e86ff26675\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"126fc856-1b05-4a3f-b0ca-f84e22cedc8b\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3e249917-d42e-4eed-89de-e492c768cfd6\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"333f691f-a2fc-4ae4-8ccd-bb8ae6f6f61b\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a14fb587-f942-4431-8907-a6d040ba5503\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"001874fc-347f-4b65-8f44-797de3e796ab\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c14988e2-c0b0-43b9-8962-f6232d907c6b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"fe5804f5-17cb-465b-8dcb-62e86ff26675\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"3b4ba021-b7fe-45ae-b5ce-f43f66f0280f\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"1ae6fd4c-2119-4d0c-b618-ada107e8657f\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"8ab1c3bc-d215-4e0c-9f8b-57f5b552e0a4\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"08b62837-f697-4159-81fb-db474f44c759\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 8 · Grammar — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Simple and Compound Subjects and Predicates<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"37349370-347e-476c-be5a-4c3b75eaf666\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 12<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: any part of the story. These sentences come from the story, not your grammar workbook.<\u002Fp>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 16.5px; line-height: 1.7;\">\n The <strong>complete subject<\u002Fstrong> is all the words that tell <em>who or what<\u002Fem>. The <strong>complete predicate<\u002Fstrong> is all the words that tell <em>what the subject does or is<\u002Fem>. A <strong>compound subject<\u002Fstrong> has two or more subjects joined by <em>and / or<\u002Fem>. A <strong>compound predicate<\u002Fstrong> has two or more predicates.<br>\n <span style=\"font-family: var(--font-body); font-size: 24px;\"><span style=\"border-bottom: 2px solid var(--color-accent);\">The cold wind<\u002Fspan> <span style=\"border-bottom: 6px double var(--color-accent);\">bit my face<\u002Fspan>.<\u002Fspan>\n <\u002Fdiv>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-2);\">In A5 students often underline only <em>boy<\u002Fem> as the subject — the complete subject is <em>The new boy in school<\u002Fem>. In B3 they usually spot the compound subject and miss the compound predicate — make them find two verbs.<\u002Fp>\n <p style=\"margin: 0;\">This unit does <strong>not<\u002Fstrong> cover compound sentences (two complete thoughts joined by and/but/or/so). That is a different skill — do not let the word “compound” pull it in here.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Underline the subject once, the predicate twice<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">5 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Find the verb first, then ask <em>who or what did that?<\u002Fem> — everything in the answer is the complete subject, and everything from the verb on is the complete predicate. The trap is stopping too early: the subject is <em>all<\u002Fem> the words that tell who, so in sentence 5 it is not “boy” but <em>the new boy in school<\u002Fem>. Every word in the sentence belongs to one side or the other.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; align-items: center; gap: var(--space-4); margin-bottom: var(--space-4); flex-wrap: wrap;\">\n <span style=\"font-size: 20px; color: var(--color-neutral-700);\">Marking mode:<\u002Fspan>\n <div style=\"display: flex; border: 1px solid var(--color-divider); border-radius: var(--radius-md); overflow: hidden;\">\n <button sc-camel-on-click=\"{{ modeS }}\" style=\"min-height: 50px; padding: 8px 22px; font-family: var(--font-body); font-size: 21px; border: none; cursor: pointer; background: {{ modeSBg }}; color: var(--color-text);\"><span style=\"border-bottom: 2px solid var(--color-accent);\">Subject<\u002Fspan> — once<\u002Fbutton>\n <button sc-camel-on-click=\"{{ modeP }}\" style=\"min-height: 50px; padding: 8px 22px; font-family: var(--font-body); font-size: 21px; border: none; border-left: 1px solid var(--color-divider); cursor: pointer; background: {{ modePBg }}; color: var(--color-text);\"><span style=\"border-bottom: 6px double var(--color-accent);\">Predicate<\u002Fspan> — twice<\u002Fbutton>\n <\u002Fdiv>\n <span style=\"font-size: 18px; color: var(--color-neutral-600); font-style: italic;\">Tap words to underline them, then check. Tap again to clear a word.<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ sentences }}\" as=\"sn\" hint-placeholder-count=\"5\">\n <div style=\"border: {{ sn.border }}; border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); animation: {{ sn.anim }};\">\n <div style=\"display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;\">\n <span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px;\">{{ sn.n }}.<\u002Fspan>\n <div style=\"display: flex; gap: 10px; flex-wrap: wrap; align-items: baseline; flex: 1;\">\n <sc-for list=\"{{ sn.words }}\" as=\"w\" hint-placeholder-count=\"6\">\n <button sc-camel-on-click=\"{{ w.pick }}\" style=\"font-family: var(--font-body); font-size: 32px; font-weight: 400; background: transparent; border: none; cursor: pointer; padding: 4px 2px 6px; min-height: 44px; color: var(--color-text); border-bottom: {{ w.line }};\">{{ w.text }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ sn.open }}\" hint-placeholder-val=\"{{ true }}\">\n <button sc-camel-on-click=\"{{ sn.check }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 10px 20px; min-height: 48px; font-family: var(--font-body); font-size: 19px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Check<\u002Fbutton>\n <\u002Fsc-if>\n <sc-if value=\"{{ sn.done }}\" hint-placeholder-val=\"{{ false }}\">\n <span style=\"font-family: var(--font-body); font-size: 22px; color: var(--color-accent-700);\">✓<\u002Fspan>\n <\u002Fsc-if>\n <\u002Fdiv>\n <sc-if value=\"{{ sn.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-2);\">{{ sn.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Compound subject, compound predicate, or both?<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">4 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainB }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainB }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Count two things: <em>how many who-or-whats<\u002Fem>, and <em>how many verbs<\u002Fem>. Two subjects sharing one verb → compound subject. One subject doing two things → compound predicate. Two of each → both. The “and” alone tells you nothing — ask what it is joining.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ bItems }}\" as=\"b\" hint-placeholder-count=\"4\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); animation: {{ b.anim }};\">\n <div style=\"font-family: var(--font-body); font-size: 28px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; font-family: var(--font-body); margin-right: 8px;\">{{ b.n }}.<\u002Fspan>{{ b.text }}<\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-3); flex-wrap: wrap;\">\n <sc-for list=\"{{ b.opts }}\" as=\"o\" hint-placeholder-count=\"3\">\n <button sc-camel-on-click=\"{{ o.pick }}\" style=\"min-height: 48px; padding: 8px 22px; font-family: var(--font-body); font-size: 21px; border: {{ o.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ o.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ o.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ b.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-3);\">{{ b.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">C<\u002Fspan>Write three sentences about anyone in the story<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">3 points<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Build each one together on the board, check it against the test, then award the point.<\u002Fp>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ cItems }}\" as=\"c\" hint-placeholder-count=\"3\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\">{{ c.q }}<\u002Fdiv>\n <textarea rows=\"2\" placeholder=\"Type the class sentence here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ c.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ c.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ c.award }}\" style=\"background: {{ c.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ c.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ c.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px; line-height: 1.65;\"><em style=\"color: var(--color-accent-700);\">Example:<\u002Fem> {{ c.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"dd4d1f1c-0a88-4bb5-8fd5-4d889a034b87\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { mode: 'S', marks: {}, locked: {}, tries: {}, shakeA: null, bDone: {}, bTries: {}, shakeB: null, cRevealed: {}, cAwarded: {}, earned: 0, teacher: false, explainA: false, explainB: false };\n SENT = [\n [[\"Pacy's\", 'best', 'friend', 'moved', 'to', 'California.'], 3, 'The subject is every word that tells who — all three of them.'],\n [['The', 'cold', 'wind', 'bit', 'my', 'face.'], 3, 'What bit? Not just \"wind\" — the whole answer.'],\n [['Dad', 'laughed', 'at', 'my', 'frozen', 'nose.'], 1, 'Only one word tells who here. Everything else is what he did.'],\n [['Two', 'philosophers', 'walked', 'across', 'a', 'bridge.'], 2, 'Who walked? Two words answer that.'],\n [['The', 'new', 'boy', 'in', 'school', 'lives', 'in', \"Melody's\", 'old', 'house.'], 5, 'Not just \"boy\" — the complete subject is all five words that tell who.'],\n ];\n B = [\n ['Becky and Charlotte watched the football game.', 'compound subject', 'Two whos, one verb.'],\n ['Pacy took a deep breath and gathered her tiger strength.', 'compound predicate', 'One who — but count the verbs.'],\n ['Melody and her mom packed their things and drove away.', 'both', 'Count the whos AND the verbs. Two of each?'],\n ['The snow fell and covered the whole town.', 'compound predicate', 'One subject, doing two things.'],\n ];\n BOPTS = ['compound subject', 'compound predicate', 'both'];\n C = [\n ['1 \\u00b7 A sentence with a compound subject (two whos, one verb).', 'Pacy and Melody talked on the phone for an hour.'],\n ['2 \\u00b7 A sentence with a compound predicate (one who, two verbs).', 'Dad smiled and told another story.'],\n ['3 \\u00b7 A simple sentence (one subject, one predicate).', 'Pacy drew a panda.'],\n ];\n shakeId(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const sentences = this.SENT.map(([wordsArr, split, hint], si) => {\n const locked = !!s.locked[si];\n const marks = s.marks[si] || {};\n const words = wordsArr.map((text, wi) => {\n const mk = locked ? (wi < split ? 'S' : 'P') : marks[wi];\n return { text,\n line: mk === 'S' ? '2px solid var(--color-accent)' : mk === 'P' ? '6px double var(--color-accent)' : '2px solid transparent',\n pick: () => {\n if (locked) return;\n this.setState(p => {\n const m = { ...(p.marks[si] || {}) };\n if (m[wi] === p.mode) delete m[wi]; else m[wi] = p.mode;\n return { marks: { ...p.marks, [si]: m } };\n });\n } };\n });\n return { n: si + 1, words, hint, done: locked, open: !locked,\n border: locked ? '1px solid var(--color-accent-300)' : '1px solid var(--color-divider)',\n anim: s.shakeA === si ? 'omShake 0.4s' : 'none',\n showHint: !locked && (s.tries[si] || 0) >= 2,\n check: () => {\n const m = s.marks[si] || {};\n const ok = wordsArr.every((_, wi) => m[wi] === (wi < split ? 'S' : 'P'));\n if (ok) this.setState(p => ({ locked: { ...p.locked, [si]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ tries: { ...p.tries, [si]: (p.tries[si] || 0) + 1 } })); this.shakeId('shakeA', si); }\n } };\n });\n const bItems = this.B.map(([text, ans, hint], i) => {\n const done = !!s.bDone[i];\n return { n: i + 1, text, hint,\n anim: s.shakeB === i ? 'omShake 0.4s' : 'none',\n showHint: !done && (s.bTries[i] || 0) >= 2,\n opts: this.BOPTS.map(label => ({ label,\n bg: done && label === ans ? 'var(--color-accent-100)' : 'transparent',\n border: done && label === ans ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (done) return;\n if (label === ans) this.setState(p => ({ bDone: { ...p.bDone, [i]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ bTries: { ...p.bTries, [i]: (p.bTries[i] || 0) + 1 } })); this.shakeId('shakeB', i); }\n } })) };\n });\n const cItems = this.C.map(([q, model], i) => {\n const revealed = !!s.cRevealed[i], awarded = !!s.cAwarded[i];\n return { q, model, revealed,\n revealLabel: revealed ? 'Hide example' : 'Show an example',\n awardLabel: awarded ? '\\u2713 Point awarded' : 'Award point',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ cRevealed: { ...p.cRevealed, [i]: !revealed } })),\n award: () => this.setState(p => ({ cAwarded: { ...p.cAwarded, [i]: !awarded }, earned: p.earned + (awarded ? -1 : 1) })) };\n });\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA, explainB: s.explainB,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n toggleExplainB: () => this.setState(p => ({ explainB: !p.explainB })),\n modeS: () => this.setState({ mode: 'S' }), modeP: () => this.setState({ mode: 'P' }),\n modeSBg: s.mode === 'S' ? 'var(--color-accent-100)' : 'transparent',\n modePBg: s.mode === 'P' ? 'var(--color-accent-100)' : 'transparent',\n sentences, bItems, cItems };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"4db3298f-06ff-4cf8-9dbc-118544ef5ef8": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "f6ccfc67-c873-431f-b725-148a2daf3348": {"mime": "image/png", "compressed": false, "data": "@@8c464739aa8721@@"}, "ea283e80-c7cb-421f-8150-0b490b3a166c": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "3af77728-fd65-4a64-8f5e-e164e9752c15": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "e7380b1e-9dc0-4b0f-b0b2-a482755f9247": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "594c5507-badb-4095-9e83-df12022c59ae": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "79500bb4-1d08-4b83-a309-b4fafa73187b": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "be3b2caf-3e43-4e1b-8215-19ff3714ff16": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "a29ffb47-c1dd-4075-890e-4ed9b6bc8ca8": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "e5c7abbd-2a29-41bf-ac0a-2a5d64b751a2": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "9af48aea-04cd-4dfb-bb44-65239877dce9": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "7ea19ed7-0ff2-447e-8e72-c7279fd15d2a": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "2ce692a0-23bf-469e-98e4-d1b688660090": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "df8c53c9-3904-42ce-b2a5-d4dc821f3758": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "e30ffe30-04a2-42a7-96d3-257cb5c454d5": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "5d2126c1-1381-4982-87d7-ed85e27dff22": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "24c5cb71-e368-4700-a789-b189aec64df5": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "8e198a58-6547-483f-a068-997f1589c899": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "d32a4867-b2b6-46cd-ba5a-9936958ec1de": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "0d2f5437-8c56-469b-835a-da2614ac497e": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "1de20e45-7a0e-4a55-a184-719ad37d8751": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "a2190e2b-f9b9-4e05-babb-6e53552d047f": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "3aa6bb42-9195-4dde-887d-73100ae8f9f8": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "14988e85-2fe7-49ff-bd9d-3705bb1a02c2": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "dc06aa07-f35d-4d21-8bfd-7f131409c0bb": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "55ec6edb-60b9-445a-96fa-e07496e58ce3": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f08c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f08c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f08c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f08c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"3af77728-fd65-4a64-8f5e-e164e9752c15\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e7380b1e-9dc0-4b0f-b0b2-a482755f9247\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"594c5507-badb-4095-9e83-df12022c59ae\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"79500bb4-1d08-4b83-a309-b4fafa73187b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"be3b2caf-3e43-4e1b-8215-19ff3714ff16\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a29ffb47-c1dd-4075-890e-4ed9b6bc8ca8\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e7380b1e-9dc0-4b0f-b0b2-a482755f9247\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"594c5507-badb-4095-9e83-df12022c59ae\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"79500bb4-1d08-4b83-a309-b4fafa73187b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"be3b2caf-3e43-4e1b-8215-19ff3714ff16\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a29ffb47-c1dd-4075-890e-4ed9b6bc8ca8\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e5c7abbd-2a29-41bf-ac0a-2a5d64b751a2\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"9af48aea-04cd-4dfb-bb44-65239877dce9\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"7ea19ed7-0ff2-447e-8e72-c7279fd15d2a\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2ce692a0-23bf-469e-98e4-d1b688660090\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"df8c53c9-3904-42ce-b2a5-d4dc821f3758\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e30ffe30-04a2-42a7-96d3-257cb5c454d5\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5d2126c1-1381-4982-87d7-ed85e27dff22\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e5c7abbd-2a29-41bf-ac0a-2a5d64b751a2\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"9af48aea-04cd-4dfb-bb44-65239877dce9\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"7ea19ed7-0ff2-447e-8e72-c7279fd15d2a\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2ce692a0-23bf-469e-98e4-d1b688660090\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"df8c53c9-3904-42ce-b2a5-d4dc821f3758\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e30ffe30-04a2-42a7-96d3-257cb5c454d5\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5d2126c1-1381-4982-87d7-ed85e27dff22\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"24c5cb71-e368-4700-a789-b189aec64df5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8e198a58-6547-483f-a068-997f1589c899\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d32a4867-b2b6-46cd-ba5a-9936958ec1de\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0d2f5437-8c56-469b-835a-da2614ac497e\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"1de20e45-7a0e-4a55-a184-719ad37d8751\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a2190e2b-f9b9-4e05-babb-6e53552d047f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3aa6bb42-9195-4dde-887d-73100ae8f9f8\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"24c5cb71-e368-4700-a789-b189aec64df5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8e198a58-6547-483f-a068-997f1589c899\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d32a4867-b2b6-46cd-ba5a-9936958ec1de\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0d2f5437-8c56-469b-835a-da2614ac497e\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"1de20e45-7a0e-4a55-a184-719ad37d8751\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a2190e2b-f9b9-4e05-babb-6e53552d047f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3aa6bb42-9195-4dde-887d-73100ae8f9f8\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"14988e85-2fe7-49ff-bd9d-3705bb1a02c2\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"dc06aa07-f35d-4d21-8bfd-7f131409c0bb\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"55ec6edb-60b9-445a-96fa-e07496e58ce3\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"ea283e80-c7cb-421f-8150-0b490b3a166c\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 9 · Grammar — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Subject–Verb Agreement<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"4db3298f-06ff-4cf8-9dbc-118544ef5ef8\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: any part of the story. These sentences come from the story, not your grammar workbook.<\u002Fp>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 20px; line-height: 1.7;\">\n Add <strong>-s<\u002Fstrong> to the verb when the subject is <em>one<\u002Fem> person or thing. Do <strong>not<\u002Fstrong> add -s when the subject is more than one.<br>\n <span style=\"font-family: var(--font-body); font-size: 24px;\">Pacy <span style=\"color: #263d5c; border-bottom: 2px solid #33517a;\">draws<\u002Fspan> every day. · The girls <span style=\"color: #8e2a24; border-bottom: 2px solid #b5372f;\">draw<\u002Fspan> every day.<\u002Fspan>\n <\u002Fdiv>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0;\">A3 — <em>The two philosophers<\u002Fem> looks long and formal, and students reach for <em>argues<\u002Fem>. Cover the extra words and ask “how many?” A5 — <em>A tiger<\u002Fem> is singular, so it takes <em>relies<\u002Fem>; the y→ies spelling trips them separately from the agreement rule.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Tap the verb that agrees<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">5 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Cover everything except the subject and ask: <em>how many?<\u002Fem> One → the verb takes -s. More than one → no -s. Long subjects are the trap: “The two philosophers” is still just a <em>they<\u002Fem>. And remember: he, she and it always take the -s form.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ aItems }}\" as=\"a\" hint-placeholder-count=\"5\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4); animation: {{ a.anim }};\">\n <div style=\"display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;\">\n <span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px;\">{{ a.n }}.<\u002Fspan>\n <span style=\"font-family: var(--font-body); font-size: 28px;\">{{ a.pre }}<\u002Fspan>\n <sc-for list=\"{{ a.opts }}\" as=\"o\" hint-placeholder-count=\"2\">\n <button sc-camel-on-click=\"{{ o.pick }}\" style=\"min-height: 50px; padding: 6px 20px; font-family: var(--font-body); font-size: 26px; border: {{ o.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ o.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ o.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <span style=\"font-family: var(--font-body); font-size: 28px;\">{{ a.post }}<\u002Fspan>\n <\u002Fdiv>\n <sc-if value=\"{{ a.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-2);\">{{ a.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Fix the verb that does not agree<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">3 points<\u002Fdiv>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ bItems }}\" as=\"b\" hint-placeholder-count=\"3\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4);\">\n <div style=\"font-size: 26px; font-family: var(--font-body); margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ b.n }}.<\u002Fspan>{{ b.text }}<\u002Fdiv>\n <textarea rows=\"1\" placeholder=\"Rewrite it correctly…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ b.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ b.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ b.award }}\" style=\"background: {{ b.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ b.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ b.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 22px;\"><em style=\"color: var(--color-accent-700);\">Model:<\u002Fem> {{ b.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">C<\u002Fspan>Write two sentences about Pacy<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">2 points · present tense<\u002Fdiv>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ cItems }}\" as=\"c\" hint-placeholder-count=\"2\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\">{{ c.q }}<\u002Fdiv>\n <textarea rows=\"1\" placeholder=\"Type the class sentence here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ c.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ c.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ c.award }}\" style=\"background: {{ c.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ c.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ c.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 22px;\"><em style=\"color: var(--color-accent-700);\">Example:<\u002Fem> {{ c.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"f6ccfc67-c873-431f-b725-148a2daf3348\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { aDone: {}, aTries: {}, shakeA: null, bRevealed: {}, bAwarded: {}, cRevealed: {}, cAwarded: {}, earned: 0, teacher: false, explainA: false };\n A = [\n ['Pacy', 'to be an author and illustrator.', ['want', 'wants'], 'wants', 'Pacy is one person \\u2014 she takes the -s form.'],\n ['Her parents', 'about the cold door.', ['worry', 'worries'], 'worry', 'Parents = more than one. No -s.'],\n ['The two philosophers', 'about the fish.', ['argue', 'argues'], 'argue', 'Cover the extra words: how many philosophers? Two \\u2014 no -s.'],\n ['Becky and Charlotte', 'Dun-Wei a mean name.', ['call', 'calls'], 'call', 'Two girls \\u2014 a they. No -s.'],\n ['A tiger', 'on his own courage.', ['rely', 'relies'], 'relies', 'A tiger is ONE \\u2014 and rely becomes relies (y \\u2192 ies).'],\n ];\n B = [\n ['Melody live in California now.', 'Melody lives in California now.'],\n ['The boys plays football in the snow.', 'The boys play football in the snow.'],\n ['Dad tell Pacy a story about the tiger.', 'Dad tells Pacy a story about the tiger.'],\n ];\n C = [\n ['1 \\u00b7 A singular subject (one person or thing).', 'Pacy practises her drawing every night.'],\n ['2 \\u00b7 A plural subject (more than one).', 'Pacy and Melody talk on the phone every week.'],\n ];\n shakeId(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const aItems = this.A.map(([pre, post, opts, ans, hint], i) => {\n const done = !!s.aDone[i];\n return { n: i + 1, pre, post, hint,\n anim: s.shakeA === i ? 'omShake 0.4s' : 'none',\n showHint: !done && (s.aTries[i] || 0) >= 2,\n opts: opts.map(label => ({ label,\n bg: done && label === ans ? 'var(--color-accent-100)' : 'transparent',\n border: done && label === ans ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (done) return;\n if (label === ans) this.setState(p => ({ aDone: { ...p.aDone, [i]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ aTries: { ...p.aTries, [i]: (p.aTries[i] || 0) + 1 } })); this.shakeId('shakeA', i); }\n } })) };\n });\n const mk = (arr, revKey, awKey) => arr.map(([text, model], i) => {\n const revealed = !!s[revKey][i], awarded = !!s[awKey][i];\n return { n: i + 1, text, q: text, model, revealed,\n revealLabel: revealed ? 'Hide model' : 'Reveal model',\n awardLabel: awarded ? '\\u2713 Point awarded' : 'Award point',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ [revKey]: { ...p[revKey], [i]: !revealed } })),\n award: () => this.setState(p => ({ [awKey]: { ...p[awKey], [i]: !awarded }, earned: p.earned + (awarded ? -1 : 1) })) };\n });\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n aItems, bItems: mk(this.B, 'bRevealed', 'bAwarded'), cItems: mk(this.C, 'cRevealed', 'cAwarded') };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"59fc3a80-f24a-4bb7-815c-b52400ab8ee5": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "24db024e-3480-4b8b-8b44-049a153e3a37": {"mime": "image/png", "compressed": false, "data": "@@8c464739aa8721@@"}, "57b77b11-de32-41a3-bbbe-e012769f398e": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "9c9db67f-bac2-4f9a-a719-928a887293ad": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "0798fca5-2e11-4e1d-8aef-a5d590a08f28": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "ea38160e-a955-4c4e-962b-e3829a4899b8": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "366bb0b4-3a78-4026-89ba-f579c1ba60b5": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "f8d22001-57f4-4595-9a19-6bd62963f05b": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "144c0d21-bda9-4177-a8c4-6fa4f2148735": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "204d0d06-11e4-477b-82fc-c6ec688553af": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "d6b2d77e-35dc-4a50-9699-5666c5caa6bc": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "bcfdd922-c1e0-4abc-ada1-65377d74aa35": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "290fadf3-6034-48c7-b6ec-d4e195c95dd6": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "c8476579-c701-43bc-8946-6df55be95f1a": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "2caeea53-9b34-4fae-8c23-ec2c4a4fecba": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "6b83b387-7169-46ac-b129-6871bcaad6dd": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "01f7d4da-cdb4-4cd2-850a-8eb58962910c": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "4d9c008e-6a8a-4ae2-9fbf-338601ad90c1": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "8d389947-5855-4141-bdfe-0a9e42350e83": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "77ab6425-2d58-41a7-8173-59307879a4d2": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "0cb66f5a-33b7-4e42-a701-9b1ce30d6916": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "697c501a-7e0a-4f59-9b00-946165e20d57": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "98ed0a4b-c13c-4635-89b4-05c79f83ae40": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "5b0ec179-c517-414c-8405-464c1d225def": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "c806ead9-9a26-46e2-8061-2a933844578f": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "d9e1511e-d5c7-45e4-b07c-55898e0f9735": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f09c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f09c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f09c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f09c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"9c9db67f-bac2-4f9a-a719-928a887293ad\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0798fca5-2e11-4e1d-8aef-a5d590a08f28\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ea38160e-a955-4c4e-962b-e3829a4899b8\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"366bb0b4-3a78-4026-89ba-f579c1ba60b5\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f8d22001-57f4-4595-9a19-6bd62963f05b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"144c0d21-bda9-4177-a8c4-6fa4f2148735\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0798fca5-2e11-4e1d-8aef-a5d590a08f28\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ea38160e-a955-4c4e-962b-e3829a4899b8\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"366bb0b4-3a78-4026-89ba-f579c1ba60b5\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f8d22001-57f4-4595-9a19-6bd62963f05b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"144c0d21-bda9-4177-a8c4-6fa4f2148735\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"204d0d06-11e4-477b-82fc-c6ec688553af\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d6b2d77e-35dc-4a50-9699-5666c5caa6bc\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"bcfdd922-c1e0-4abc-ada1-65377d74aa35\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"290fadf3-6034-48c7-b6ec-d4e195c95dd6\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c8476579-c701-43bc-8946-6df55be95f1a\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2caeea53-9b34-4fae-8c23-ec2c4a4fecba\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6b83b387-7169-46ac-b129-6871bcaad6dd\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"204d0d06-11e4-477b-82fc-c6ec688553af\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d6b2d77e-35dc-4a50-9699-5666c5caa6bc\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"bcfdd922-c1e0-4abc-ada1-65377d74aa35\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"290fadf3-6034-48c7-b6ec-d4e195c95dd6\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c8476579-c701-43bc-8946-6df55be95f1a\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2caeea53-9b34-4fae-8c23-ec2c4a4fecba\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6b83b387-7169-46ac-b129-6871bcaad6dd\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"01f7d4da-cdb4-4cd2-850a-8eb58962910c\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4d9c008e-6a8a-4ae2-9fbf-338601ad90c1\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8d389947-5855-4141-bdfe-0a9e42350e83\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"77ab6425-2d58-41a7-8173-59307879a4d2\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0cb66f5a-33b7-4e42-a701-9b1ce30d6916\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"697c501a-7e0a-4f59-9b00-946165e20d57\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"98ed0a4b-c13c-4635-89b4-05c79f83ae40\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"01f7d4da-cdb4-4cd2-850a-8eb58962910c\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4d9c008e-6a8a-4ae2-9fbf-338601ad90c1\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8d389947-5855-4141-bdfe-0a9e42350e83\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"77ab6425-2d58-41a7-8173-59307879a4d2\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0cb66f5a-33b7-4e42-a701-9b1ce30d6916\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"697c501a-7e0a-4f59-9b00-946165e20d57\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"98ed0a4b-c13c-4635-89b4-05c79f83ae40\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"5b0ec179-c517-414c-8405-464c1d225def\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c806ead9-9a26-46e2-8061-2a933844578f\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"d9e1511e-d5c7-45e4-b07c-55898e0f9735\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"57b77b11-de32-41a3-bbbe-e012769f398e\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 10 · Grammar — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Four Kinds of Sentences<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"59fc3a80-f24a-4bb7-815c-b52400ab8ee5\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 12<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: any part of the story. These sentences come from the story, not your grammar workbook.<\u002Fp>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 16.5px; line-height: 1.8;\">\n <strong>Declarative<\u002Fstrong> tells something. (.) · <strong>Interrogative<\u002Fstrong> asks. (?) · <strong>Imperative<\u002Fstrong> gives a command. (.) · <strong>Exclamatory<\u002Fstrong> shows strong feeling. (!)\n <\u002Fdiv>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0;\">B2 — “look at that throw” begins like a command, and some students will say imperative. In the story Charlotte shouts it in excitement, so the exclamation point is correct. Both readings are defensible; accept imperative if the student writes a full stop and can explain why.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Real lines from the story — name each kind<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">6 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">The end mark is a clue, not the answer — ask what the sentence <em>does<\u002Fem>: does it tell, ask, order, or burst out? Two of these lines end in ! and one has no drama at all (“Dad nodded.”) — a plain telling sentence is declarative no matter how short. And a command aims at <em>you<\u002Fem>, even when the word you is invisible: “Don’t worry…”<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ aItems }}\" as=\"a\" hint-placeholder-count=\"6\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); animation: {{ a.anim }};\">\n <div style=\"font-family: var(--font-body); font-size: 30px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; font-family: var(--font-body); margin-right: 8px;\">{{ a.n }}.<\u002Fspan>{{ a.text }}<\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-3); flex-wrap: wrap;\">\n <sc-for list=\"{{ a.opts }}\" as=\"o\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ o.pick }}\" style=\"min-height: 48px; padding: 8px 22px; font-family: var(--font-body); font-size: 21px; border: {{ o.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ o.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ o.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ a.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-3);\">{{ a.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Add the end mark, then name the kind<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">3 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainB }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainB }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Read the sentence aloud with feeling before choosing — the voice finds the mark before the eye does. A question curls up at the end; a shout has “Wow” energy; plain telling falls flat at a full stop. Then the kind follows the mark you chose.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ bItems }}\" as=\"b\" hint-placeholder-count=\"3\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); animation: {{ b.anim }};\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3);\">\n <span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px;\">{{ b.n }}.<\u002Fspan>\n <span style=\"font-family: var(--font-body); font-size: 30px;\">{{ b.text }}<\u002Fspan>\n <span style=\"display: inline-block; min-width: 34px; border-bottom: 2px solid {{ b.markLine }}; text-align: center; font-family: var(--font-body); font-size: 30px; color: var(--color-accent-700);\">{{ b.mark }}<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-4); flex-wrap: wrap; align-items: center;\">\n <div style=\"display: flex; gap: var(--space-2);\">\n <sc-for list=\"{{ b.marks }}\" as=\"mk\" hint-placeholder-count=\"3\">\n <button sc-camel-on-click=\"{{ mk.pick }}\" style=\"min-width: 50px; min-height: 50px; font-family: var(--font-body); font-size: 30px; border: {{ mk.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ mk.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ mk.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ b.markDone }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"display: flex; gap: var(--space-2); flex-wrap: wrap;\">\n <sc-for list=\"{{ b.kinds }}\" as=\"k\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ k.pick }}\" style=\"min-height: 48px; padding: 8px 18px; font-family: var(--font-body); font-size: 20px; border: {{ k.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ k.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ k.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsc-if>\n <sc-if value=\"{{ b.done }}\" hint-placeholder-val=\"{{ false }}\">\n <span style=\"font-family: var(--font-body); font-size: 22px; color: var(--color-accent-700);\">✓<\u002Fspan>\n <\u002Fsc-if>\n <\u002Fdiv>\n <sc-if value=\"{{ b.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-3);\">{{ b.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">C<\u002Fspan>Rewrite each sentence as the kind in brackets<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">3 points<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Rewrite together on the board, reveal the model, then award the point.<\u002Fp>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ cItems }}\" as=\"c\" hint-placeholder-count=\"3\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4);\">\n <div style=\"font-size: 24px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ c.n }}.<\u002Fspan>{{ c.q }} <em style=\"color: var(--color-accent-700);\">({{ c.kind }})<\u002Fem><\u002Fdiv>\n <textarea rows=\"1\" placeholder=\"Type the class answer here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ c.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ c.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ c.award }}\" style=\"background: {{ c.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ c.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ c.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 20px;\"><em style=\"color: var(--color-accent-700);\">Model:<\u002Fem> {{ c.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"24db024e-3480-4b8b-8b44-049a153e3a37\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { aDone: {}, aTries: {}, shakeA: null, bMark: {}, bKind: {}, bTries: {}, shakeB: null, cRevealed: {}, cAwarded: {}, earned: 0, teacher: false, explainA: false, explainB: false };\n KINDS = ['declarative', 'interrogative', 'imperative', 'exclamatory'];\n A = [\n ['\\u201cLots of snow today!\\u201d', 'exclamatory', 'It bursts out — hear the excitement.'],\n ['\\u201cHow do you know which is which?\\u201d', 'interrogative', 'It asks — the voice curls up at the end.'],\n ['\\u201cDon\\u2019t worry, it\\u2019s not that bad.\\u201d', 'imperative', 'It tells YOU to do something — even though the word \"you\" is invisible.'],\n ['Dad nodded.', 'declarative', 'Short, but it just tells us something. No drama at all.'],\n ['\\u201cIt froze my nose!\\u201d', 'exclamatory', 'Strong feeling — the ! is doing real work.'],\n ['Melody was going to come back to visit.', 'declarative', 'It simply tells. Nobody is asked or ordered.'],\n ];\n B = [\n ['Why do you look so frightened all of a sudden', '?', 'interrogative', 'It starts with \"Why\" — what does that tell you?'],\n ['Wow, look at that throw', '!', 'exclamatory', 'Charlotte shouts this in excitement — \"Wow\" gives it away.'],\n ['The weather seemed to turn cold in a snap', '.', 'declarative', 'Read it flat — it just tells.'],\n ];\n C = [\n ['Pacy is brave.', 'interrogative', 'Is Pacy brave?'],\n ['You should not call him Dumb-Way.', 'imperative', 'Don\\u2019t call him Dumb-Way.'],\n ['Melody is coming back to visit.', 'exclamatory', 'Melody is coming back to visit!'],\n ];\n shakeId(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const aItems = this.A.map(([text, ans, hint], i) => {\n const done = !!s.aDone[i];\n return { n: i + 1, text, hint,\n anim: s.shakeA === i ? 'omShake 0.4s' : 'none',\n showHint: !done && (s.aTries[i] || 0) >= 2,\n opts: this.KINDS.map(label => ({ label,\n bg: done && label === ans ? 'var(--color-accent-100)' : 'transparent',\n border: done && label === ans ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (done) return;\n if (label === ans) this.setState(p => ({ aDone: { ...p.aDone, [i]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ aTries: { ...p.aTries, [i]: (p.aTries[i] || 0) + 1 } })); this.shakeId('shakeA', i); }\n } })) };\n });\n const bItems = this.B.map(([text, mark, kind, hint], i) => {\n const markDone = !!s.bMark[i], done = !!s.bKind[i];\n return { n: i + 1, text, hint, markDone, done,\n mark: markDone ? mark : '\\u00a0',\n markLine: markDone ? 'var(--color-accent)' : 'var(--color-neutral-400)',\n anim: s.shakeB === i ? 'omShake 0.4s' : 'none',\n showHint: !done && (s.bTries[i] || 0) >= 2,\n marks: ['.', '?', '!'].map(label => ({ label,\n bg: markDone && label === mark ? 'var(--color-accent-100)' : 'transparent',\n border: markDone && label === mark ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (markDone) return;\n if (label === mark) this.setState(p => ({ bMark: { ...p.bMark, [i]: true } }));\n else { this.setState(p => ({ bTries: { ...p.bTries, [i]: (p.bTries[i] || 0) + 1 } })); this.shakeId('shakeB', i); }\n } })),\n kinds: this.KINDS.map(label => ({ label,\n bg: done && label === kind ? 'var(--color-accent-100)' : 'transparent',\n border: done && label === kind ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (done) return;\n if (label === kind) this.setState(p => ({ bKind: { ...p.bKind, [i]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ bTries: { ...p.bTries, [i]: (p.bTries[i] || 0) + 1 } })); this.shakeId('shakeB', i); }\n } })) };\n });\n const cItems = this.C.map(([q, kind, model], i) => {\n const revealed = !!s.cRevealed[i], awarded = !!s.cAwarded[i];\n return { n: i + 1, q, kind, model, revealed,\n revealLabel: revealed ? 'Hide model' : 'Reveal model',\n awardLabel: awarded ? '\\u2713 Point awarded' : 'Award point',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ cRevealed: { ...p.cRevealed, [i]: !revealed } })),\n award: () => this.setState(p => ({ cAwarded: { ...p.cAwarded, [i]: !awarded }, earned: p.earned + (awarded ? -1 : 1) })) };\n });\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA, explainB: s.explainB,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n toggleExplainB: () => this.setState(p => ({ explainB: !p.explainB })),\n aItems, bItems, cItems };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"3e92b690-54f5-425f-ae1b-e31f014a762a": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "e277d4b7-06b2-4df8-bdb1-24a165c9b5aa": {"mime": "image/png", "compressed": false, "data": "@@84a0ed38bb4353@@"}, "4d0ffe74-5ec6-4b4f-9ec1-527a2993b9c1": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "78b8fb1f-97d6-4344-a5d0-23854ff2a6c6": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "3cd320c6-f012-4ee0-8287-5c0df47355e0": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "ce7b3db9-a544-4210-8f1d-1784ce7ede15": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "11800d71-5630-44a5-a354-ffdb57895e8b": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "e95cf056-9a76-4c46-b2e4-da203c7c1d4b": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "819832d3-aa4d-4820-8a7b-d47ebcf69e8b": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "52219adf-5d7e-4e69-b98d-a90883313d2c": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "d1e64ca1-929f-4dc5-8ee7-cc9f697b8f61": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "b218d272-1958-480c-9aa5-01db7c6bd092": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "f3717dce-af87-43f3-b015-6fb849274ba8": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "e832b6ac-d0d6-4b77-876d-1c07389d23e0": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "cd71654a-612b-46b1-b600-5e7ac9dbc923": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "09d19a26-2edc-451b-9041-5df8258e4685": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "aeb4c96d-99f1-4a8d-98b1-764d9a722653": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "673b0633-378f-47d0-834b-9b3b41d2bf20": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "9fa24514-02da-4862-8f61-c63c2ee3f7eb": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "9ba9c24b-95c0-4f65-96f1-297f60c3e424": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "c40e8528-822c-4933-8891-d38bf76d9bbf": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "56cd40d6-add9-4821-a178-3cf5c50c36fd": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "025e2227-e5bd-432e-83de-7c9df16e4384": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "6e9fa697-46bd-45c0-b47b-db6ecb6362a4": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "36df0f1c-ef9c-4083-9ccf-bb3ce44b6798": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "336b810e-cbc7-42f5-958a-ac736715ca31": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f10c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f10c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f10c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f10c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"78b8fb1f-97d6-4344-a5d0-23854ff2a6c6\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3cd320c6-f012-4ee0-8287-5c0df47355e0\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ce7b3db9-a544-4210-8f1d-1784ce7ede15\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"11800d71-5630-44a5-a354-ffdb57895e8b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e95cf056-9a76-4c46-b2e4-da203c7c1d4b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"819832d3-aa4d-4820-8a7b-d47ebcf69e8b\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3cd320c6-f012-4ee0-8287-5c0df47355e0\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ce7b3db9-a544-4210-8f1d-1784ce7ede15\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"11800d71-5630-44a5-a354-ffdb57895e8b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e95cf056-9a76-4c46-b2e4-da203c7c1d4b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"819832d3-aa4d-4820-8a7b-d47ebcf69e8b\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"52219adf-5d7e-4e69-b98d-a90883313d2c\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d1e64ca1-929f-4dc5-8ee7-cc9f697b8f61\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b218d272-1958-480c-9aa5-01db7c6bd092\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f3717dce-af87-43f3-b015-6fb849274ba8\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e832b6ac-d0d6-4b77-876d-1c07389d23e0\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cd71654a-612b-46b1-b600-5e7ac9dbc923\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"09d19a26-2edc-451b-9041-5df8258e4685\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"52219adf-5d7e-4e69-b98d-a90883313d2c\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d1e64ca1-929f-4dc5-8ee7-cc9f697b8f61\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b218d272-1958-480c-9aa5-01db7c6bd092\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f3717dce-af87-43f3-b015-6fb849274ba8\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e832b6ac-d0d6-4b77-876d-1c07389d23e0\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"cd71654a-612b-46b1-b600-5e7ac9dbc923\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"09d19a26-2edc-451b-9041-5df8258e4685\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"aeb4c96d-99f1-4a8d-98b1-764d9a722653\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"673b0633-378f-47d0-834b-9b3b41d2bf20\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"9fa24514-02da-4862-8f61-c63c2ee3f7eb\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"9ba9c24b-95c0-4f65-96f1-297f60c3e424\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c40e8528-822c-4933-8891-d38bf76d9bbf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"56cd40d6-add9-4821-a178-3cf5c50c36fd\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"025e2227-e5bd-432e-83de-7c9df16e4384\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"aeb4c96d-99f1-4a8d-98b1-764d9a722653\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"673b0633-378f-47d0-834b-9b3b41d2bf20\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"9fa24514-02da-4862-8f61-c63c2ee3f7eb\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"9ba9c24b-95c0-4f65-96f1-297f60c3e424\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c40e8528-822c-4933-8891-d38bf76d9bbf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"56cd40d6-add9-4821-a178-3cf5c50c36fd\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"025e2227-e5bd-432e-83de-7c9df16e4384\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"6e9fa697-46bd-45c0-b47b-db6ecb6362a4\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"36df0f1c-ef9c-4083-9ccf-bb3ce44b6798\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"336b810e-cbc7-42f5-958a-ac736715ca31\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"4d0ffe74-5ec6-4b4f-9ec1-527a2993b9c1\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 11 · Grammar — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Fragments and Run-Ons<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"3e92b690-54f5-425f-ae1b-e31f014a762a\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 11<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: any part of the story. These sentences come from the story, not your grammar workbook.<\u002Fp>\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 20px; line-height: 1.7;\">\n A <strong>fragment<\u002Fstrong> is missing a subject or a predicate. A <strong>run-on<\u002Fstrong> jams two complete thoughts together; a <strong>comma splice<\u002Fstrong> joins them with only a comma.\n <\u002Fdiv>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-2);\">A5 is a comma splice. Students who have learned “a comma joins sentences” from compound sentences will mark it correct. Accept “run-on” but name the comma splice for them — it is the error that survives longest into Grade 5.<\u002Fp>\n <p style=\"margin: 0;\">Joining with and/but/so is also a valid fix in Part C. Accept it, then ask for the two-sentence version as well so they can see both.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Fragment, run-on, or complete sentence?<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">6 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Ask two questions of every line: <em>Who?<\u002Fem> and <em>What did they do?<\u002Fem> Missing one answer → fragment. Two full answers crammed together with no join → run-on. A comma is not strong enough to join two complete thoughts — that is the sneaky comma splice.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ aItems }}\" as=\"a\" hint-placeholder-count=\"6\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4); animation: {{ a.anim }};\">\n <div style=\"font-family: var(--font-body); font-size: 28px; margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; font-family: var(--font-body); margin-right: 8px;\">{{ a.n }}.<\u002Fspan>{{ a.text }}<\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-3); flex-wrap: wrap;\">\n <sc-for list=\"{{ a.opts }}\" as=\"o\" hint-placeholder-count=\"3\">\n <button sc-camel-on-click=\"{{ o.pick }}\" style=\"min-height: 50px; padding: 8px 22px; font-family: var(--font-body); font-size: 21px; border: {{ o.border }}; border-radius: var(--radius-md); cursor: pointer; background: {{ o.bg }}; color: var(--color-text);\" style-hover=\"border-color: var(--color-accent);\">{{ o.label }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ a.showHint }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin-top: var(--space-3);\">{{ a.hint }}<\u002Fdiv>\n <\u002Fsc-if>\n <sc-if value=\"{{ a.done }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-size: 19px; color: var(--color-neutral-700); margin-top: var(--space-3);\">{{ a.why }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Turn each fragment into a complete sentence<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">3 points<\u002Fdiv>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ bItems }}\" as=\"b\" hint-placeholder-count=\"3\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4);\">\n <div style=\"font-size: 26px; font-family: var(--font-body); margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ b.n }}.<\u002Fspan>{{ b.text }}<\u002Fdiv>\n <textarea rows=\"1\" placeholder=\"Make it complete…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ b.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ b.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ b.award }}\" style=\"background: {{ b.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ b.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ b.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 22px;\"><em style=\"color: var(--color-accent-700);\">Example:<\u002Fem> {{ b.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">C<\u002Fspan>Fix each run-on or comma splice<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">2 points<\u002Fdiv>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ cItems }}\" as=\"c\" hint-placeholder-count=\"2\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4);\">\n <div style=\"font-size: 26px; font-family: var(--font-body); margin-bottom: var(--space-3);\"><span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ c.n }}.<\u002Fspan>{{ c.text }}<\u002Fdiv>\n <textarea rows=\"1\" placeholder=\"Split it or join it properly…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-3); margin-top: var(--space-3);\">\n <button sc-camel-on-click=\"{{ c.reveal }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">{{ c.revealLabel }}<\u002Fbutton>\n <button sc-camel-on-click=\"{{ c.award }}\" style=\"background: {{ c.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 8px 16px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ c.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ c.revealed }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"margin-top: var(--space-3); border-top: 1px solid var(--color-divider); padding-top: var(--space-3); font-size: 22px;\"><em style=\"color: var(--color-accent-700);\">Example:<\u002Fem> {{ c.model }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"e277d4b7-06b2-4df8-bdb1-24a165c9b5aa\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { aDone: {}, aTries: {}, shakeA: null, bRevealed: {}, bAwarded: {}, cRevealed: {}, cAwarded: {}, earned: 0, teacher: false, explainA: false };\n OPTS = ['fragment', 'run-on', 'complete sentence'];\n A = [\n ['Moved to California last year.', 'fragment', 'Who moved? Nobody is named.', 'Fragment \\u2014 no subject.'],\n ['Pacy wants to be an author she practises every day.', 'run-on', 'Count the complete thoughts. Two, with no join.', 'Run-on \\u2014 two complete thoughts jammed together.'],\n ['The tiger crossed the wild water.', 'complete sentence', 'Who? What did he do? Both answered.', 'Complete \\u2014 subject and predicate, one thought.'],\n ['Dad and his stories about the twelve animals.', 'fragment', 'Who is there \\u2014 but what did they DO?', 'Fragment \\u2014 no predicate.'],\n ['Melody called that night, Pacy could barely understand her.', 'run-on', 'The comma is not strong enough to join two sentences.', 'Run-on \\u2014 a comma splice: two thoughts joined by only a comma.'],\n ['Becky nodded slowly.', 'complete sentence', 'Short is fine \\u2014 does it answer who and what?', 'Complete \\u2014 short, but whole.'],\n ];\n B = [\n ['Rolled around in the snow like a gingerbread cookie.', 'Sam Mercer rolled around in the snow like a gingerbread cookie.'],\n ['The two philosophers on the bridge.', 'The two philosophers on the bridge watched an orange fish.'],\n ['Felt like a tiger inside.', 'Pacy felt like a tiger inside.'],\n ];\n C = [\n ['Pacy made resolutions she wanted to keep them.', 'Pacy made resolutions. She wanted to keep them.'],\n ['Snow fell all night, school was cancelled.', 'Snow fell all night. School was cancelled.'],\n ];\n shakeId(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const aItems = this.A.map(([text, ans, hint, why], i) => {\n const done = !!s.aDone[i];\n return { n: i + 1, text, hint, why, done,\n anim: s.shakeA === i ? 'omShake 0.4s' : 'none',\n showHint: !done && (s.aTries[i] || 0) >= 2,\n opts: this.OPTS.map(label => ({ label,\n bg: done && label === ans ? 'var(--color-accent-100)' : 'transparent',\n border: done && label === ans ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n pick: () => {\n if (done) return;\n if (label === ans) this.setState(p => ({ aDone: { ...p.aDone, [i]: true }, earned: p.earned + 1 }));\n else { this.setState(p => ({ aTries: { ...p.aTries, [i]: (p.aTries[i] || 0) + 1 } })); this.shakeId('shakeA', i); }\n } })) };\n });\n const mk = (arr, revKey, awKey) => arr.map(([text, model], i) => {\n const revealed = !!s[revKey][i], awarded = !!s[awKey][i];\n return { n: i + 1, text, model, revealed,\n revealLabel: revealed ? 'Hide example' : 'Show an example',\n awardLabel: awarded ? '\\u2713 Point awarded' : 'Award point',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n reveal: () => this.setState(p => ({ [revKey]: { ...p[revKey], [i]: !revealed } })),\n award: () => this.setState(p => ({ [awKey]: { ...p[awKey], [i]: !awarded }, earned: p.earned + (awarded ? -1 : 1) })) };\n });\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n aItems, bItems: mk(this.B, 'bRevealed', 'bAwarded'), cItems: mk(this.C, 'cRevealed', 'cAwarded') };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"942473fb-8c99-4d47-b0ed-3133179afb86": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "2dc0317a-e3bd-45e7-a53c-382601993d91": {"mime": "image/png", "compressed": false, "data": "@@f328acdee21da3@@"}, "2e8a79db-ec79-43c6-9da5-753251bcedcd": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "25128372-a4b4-4bc7-93d9-cee6bdf8ef01": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "26ccdee0-d548-42e5-8760-150aa0a1185a": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "c6c71291-8351-40da-92fc-05e31df0325e": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "3d7de799-6e13-4849-bf79-0631f2cc8af1": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "ac170980-8ad9-4590-91c4-90c10ff7de74": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "97dc97be-73ee-4b56-824c-de8365fb94e9": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "4e0554bd-feb6-47ef-8ce0-5530d15bdebe": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "1e08a323-749b-41dd-bb60-b256ca1ac07f": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "596bdae0-4ab0-41db-ada5-cbc81f01cdb3": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "e38d6b08-935c-4e5e-99fb-66f0d02288b9": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "8e517a77-89a9-491b-8f96-2df3d9f86a3f": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "0e620623-3166-4dba-9c59-88ba5904502f": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "0a658ff5-80e4-4976-b953-58bbe02c72b9": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "81c08491-3c79-4185-bb1d-25e84ffcb0d6": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "1298aa25-f797-44eb-aa31-85cc0a249051": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "2c8a531b-7c4a-4905-bf2d-5baa34c5d0bb": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "cfb93997-a53f-47cb-b1da-8f28159a6de5": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "4fe4d229-fcf0-4ded-9b72-791b917a8edb": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "8c2693bd-952a-45d4-be03-356c8f31e241": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "508f814c-3214-48f5-8341-0d594c8a87cd": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "ef9f45f5-fbb9-45da-8885-51c41b7b7478": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "c2aa2680-010f-4a3e-b1ab-0e74a37151b2": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "ca574ed0-d30d-4096-82e6-d9ae2910f706": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f11c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f11c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f11c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f11c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"25128372-a4b4-4bc7-93d9-cee6bdf8ef01\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"26ccdee0-d548-42e5-8760-150aa0a1185a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c6c71291-8351-40da-92fc-05e31df0325e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3d7de799-6e13-4849-bf79-0631f2cc8af1\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ac170980-8ad9-4590-91c4-90c10ff7de74\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"97dc97be-73ee-4b56-824c-de8365fb94e9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"26ccdee0-d548-42e5-8760-150aa0a1185a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c6c71291-8351-40da-92fc-05e31df0325e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3d7de799-6e13-4849-bf79-0631f2cc8af1\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ac170980-8ad9-4590-91c4-90c10ff7de74\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"97dc97be-73ee-4b56-824c-de8365fb94e9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4e0554bd-feb6-47ef-8ce0-5530d15bdebe\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"1e08a323-749b-41dd-bb60-b256ca1ac07f\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"596bdae0-4ab0-41db-ada5-cbc81f01cdb3\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e38d6b08-935c-4e5e-99fb-66f0d02288b9\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8e517a77-89a9-491b-8f96-2df3d9f86a3f\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0e620623-3166-4dba-9c59-88ba5904502f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0a658ff5-80e4-4976-b953-58bbe02c72b9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4e0554bd-feb6-47ef-8ce0-5530d15bdebe\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"1e08a323-749b-41dd-bb60-b256ca1ac07f\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"596bdae0-4ab0-41db-ada5-cbc81f01cdb3\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e38d6b08-935c-4e5e-99fb-66f0d02288b9\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8e517a77-89a9-491b-8f96-2df3d9f86a3f\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0e620623-3166-4dba-9c59-88ba5904502f\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0a658ff5-80e4-4976-b953-58bbe02c72b9\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"81c08491-3c79-4185-bb1d-25e84ffcb0d6\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"1298aa25-f797-44eb-aa31-85cc0a249051\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2c8a531b-7c4a-4905-bf2d-5baa34c5d0bb\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cfb93997-a53f-47cb-b1da-8f28159a6de5\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4fe4d229-fcf0-4ded-9b72-791b917a8edb\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8c2693bd-952a-45d4-be03-356c8f31e241\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"508f814c-3214-48f5-8341-0d594c8a87cd\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"81c08491-3c79-4185-bb1d-25e84ffcb0d6\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"1298aa25-f797-44eb-aa31-85cc0a249051\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2c8a531b-7c4a-4905-bf2d-5baa34c5d0bb\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"cfb93997-a53f-47cb-b1da-8f28159a6de5\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4fe4d229-fcf0-4ded-9b72-791b917a8edb\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8c2693bd-952a-45d4-be03-356c8f31e241\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"508f814c-3214-48f5-8341-0d594c8a87cd\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"ef9f45f5-fbb9-45da-8885-51c41b7b7478\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c2aa2680-010f-4a3e-b1ab-0e74a37151b2\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"ca574ed0-d30d-4096-82e6-d9ae2910f706\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"2e8a79db-ec79-43c6-9da5-753251bcedcd\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 12 · Spelling — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Short e and Long e<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"942473fb-8c99-4d47-b0ed-3133179afb86\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 24<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1400px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: course pack pp. 12–14.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-2);\"><em>pretend<\u002Fem> — the first e is a schwa, so students hear two different sounds and cannot decide. File it by the stressed syllable: <em>-tend<\u002Fem>, short e. <em>Sleeve<\u002Fem> and <em>believe<\u002Fem> end in the same sound with different spellings — that contrast is the point of the sheet.<\u002Fp>\n <p style=\"margin: 0;\">This list has 18 words, so the sheet is out of 24 rather than the 20 used for the summer unit. Scale it if you prefer a percentage that matches your other quizzes.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">A<\u002Fspan>Sort the 18 spelling words by sound and pattern<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">18 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainA }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainA }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Sort by <em>sound first, spelling second<\u002Fem>: say the word aloud — short e like <em>bed<\u002Fem>, or long e like <em>feet<\u002Fem>? Only then look at which letters make the long sound: ee, ea, ie, or e-with-a-silent-e at the end. The trap word is <em>pretend<\u002Fem> — say the stressed part, <em>-tend<\u002Fem>: short e. And notice <em>sleeve<\u002Fem> and <em>believe<\u002Fem> end in the same sound spelled two ways — that is exactly why we sort.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Drag each word into its column — or tap a word, then tap a column. Correct words stay; wrong ones bounce back.<\u002Fp>\n <div style=\"display: flex; gap: var(--space-2); flex-wrap: wrap; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-4); min-height: 60px;\">\n <sc-for list=\"{{ pool }}\" as=\"w\" hint-placeholder-count=\"18\">\n <button sc-camel-on-pointer-down=\"{{ w.down }}\" style=\"min-height: 50px; padding: 8px 20px; font-family: var(--font-body); font-size: 28px; font-weight: 600; border: {{ w.border }}; border-radius: var(--radius-md); cursor: grab; background: {{ w.bg }}; color: var(--color-text); touch-action: none; user-select: none; animation: {{ w.anim }};\">{{ w.word }}<\u002Fbutton>\n <\u002Fsc-for>\n <sc-if value=\"{{ poolEmpty }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-family: var(--font-body); font-style: italic; font-size: 26px; color: var(--color-accent-700); padding: 8px;\">All eighteen sorted ✓<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fdiv>\n <div style=\"display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--space-3);\">\n <sc-for list=\"{{ cols }}\" as=\"c\" hint-placeholder-count=\"5\">\n <div data-col=\"{{ c.id }}\" sc-camel-on-click=\"{{ c.tap }}\" style=\"border: {{ c.border }}; border-radius: var(--radius-md); padding: var(--space-3); min-height: 300px; cursor: pointer; animation: {{ c.anim }};\">\n <div style=\"border-bottom: 2px solid {{ c.hex }}; background: {{ c.tint }}; border-radius: var(--radius-sm, 4px); padding: var(--space-2); margin-bottom: var(--space-3); pointer-events: none;\">\n <div style=\"font-family: var(--font-body); font-size: 26px; font-weight: 600; color: {{ c.deep }};\">{{ c.label }}<\u002Fdiv>\n <div style=\"font-size: 13.5px; color: var(--color-neutral-600); font-variant-numeric: tabular-nums;\">as in <em>{{ c.example }}<\u002Fem> · {{ c.count }}<\u002Fdiv>\n <\u002Fdiv>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-2); pointer-events: none;\">\n <sc-for list=\"{{ c.placed }}\" as=\"p\" hint-placeholder-count=\"2\">\n <div style=\"font-family: var(--font-body); font-size: 26px; font-weight: 600; color: {{ c.deep }}; border: 1px solid {{ c.hex }}; background: {{ c.tint }}; border-radius: var(--radius-md); padding: 6px 14px;\">{{ p.word }}<\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <sc-if value=\"{{ hintWord }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"font-style: italic; color: #8e2a24; font-size: 20px; margin-top: var(--space-3);\">{{ hintText }}<\u002Fdiv>\n <\u002Fsc-if>\n <sc-if value=\"{{ dragging }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"position: fixed; left: 0; top: 0; transform: translate({{ dragX }}px, {{ dragY }}px) translate(-50%, -50%); pointer-events: none; z-index: 50; font-family: var(--font-body); font-size: 28px; font-weight: 600; padding: 8px 20px; border: 1px solid var(--color-accent); background: var(--color-accent-100); border-radius: var(--radius-md); box-shadow: var(--shadow-md);\">{{ dragWord }}<\u002Fdiv>\n <\u002Fsc-if>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">B<\u002Fspan>Write the best spelling word in each sentence<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">6 points<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplainB }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explainB }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Read the whole sentence aloud with a hum where the blank is, then try a candidate: does it make sense <em>and<\u002Fem> sound right? Two traps sound alike: the fire <em>chief<\u002Fem> (a person) and a <em>steep<\u002Fem> mountain (not “steam”). Meaning chooses the word; the sort from Part A tells you how to spell it.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <p style=\"margin: 0 0 var(--space-4); color: var(--color-neutral-700); font-size: 20px;\">Tap a blank, then tap the word that belongs in it.<\u002Fp>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-4);\">\n <sc-for list=\"{{ bItems }}\" as=\"b\" hint-placeholder-count=\"6\">\n <div style=\"font-size: 24px; line-height: 1.6; animation: {{ b.anim }};\">\n <span style=\"font-variant-numeric: tabular-nums; color: var(--color-neutral-600); font-size: 19px; margin-right: 8px;\">{{ b.n }}.<\u002Fspan>\n <span>{{ b.pre }}<\u002Fspan>\n <button sc-camel-on-click=\"{{ b.pick }}\" style=\"display: inline-block; min-width: 130px; min-height: 40px; margin: 0 4px; padding: 2px 14px; font-family: var(--font-body); font-weight: 600; font-size: 26px; color: var(--color-accent-700); background: {{ b.bg }}; border: none; border-bottom: 2px solid {{ b.lineColor }}; border-radius: var(--radius-sm, 4px) var(--radius-sm, 4px) 0 0; cursor: pointer; vertical-align: baseline;\">{{ b.fill }}<\u002Fbutton>\n <span>{{ b.post }}<\u002Fspan>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <div style=\"display: flex; gap: var(--space-3); flex-wrap: wrap; border-top: 1px solid var(--color-divider); padding-top: var(--space-4);\">\n <sc-for list=\"{{ bank }}\" as=\"k\" hint-placeholder-count=\"6\">\n <button sc-camel-on-click=\"{{ k.pick }}\" style=\"min-height: 48px; padding: 8px 22px; font-family: var(--font-body); font-size: 26px; font-weight: 600; border: 1px solid var(--color-divider); border-radius: var(--radius-md); cursor: pointer; background: transparent; color: {{ k.color }}; text-decoration: {{ k.deco }};\" style-hover=\"border-color: var(--color-accent); background: var(--color-accent-100);\">{{ k.word }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"2dc0317a-e3bd-45e7-a53c-382601993d91\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 120px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { placed: {}, tries: {}, sel: null, drag: null, shakeCol: null, shakeWord: null, hintWord: null, bSel: null, bDone: {}, shakeB: null, earned: 0, teacher: false, explainA: false, explainB: false };\n COLS = [\n { id: 'short', label: 'short e', example: 'bed', total: 5, hex: '#33517a', tint: '#e2e9f3', deep: '#263d5c' },\n { id: 'ee', label: 'long e: ee', example: 'feet', total: 4, hex: '#b5372f', tint: '#f7e3e1', deep: '#8e2a24' },\n { id: 'ea', label: 'long e: ea', example: 'leaf', total: 6, hex: '#c9973f', tint: '#f4e7d3', deep: '#7c5520' },\n { id: 'ie', label: 'long e: ie', example: 'chief', total: 2, hex: '#33517a', tint: '#e2e9f3', deep: '#263d5c' },\n { id: 'e_e', label: 'long e: e_e', example: 'complete', total: 1, hex: '#b5372f', tint: '#f7e3e1', deep: '#8e2a24' },\n ];\n ANSWER = { chief: 'ie', steam: 'ea', member: 'short', reason: 'ea', speed: 'ee', west: 'short', sleeve: 'ee', believe: 'ie', complete: 'e_e', fresh: 'short', pretend: 'short', cheap: 'ea', greed: 'ee', eager: 'ea', gleam: 'ea', beast: 'ea', shelf: 'short', steep: 'ee' };\n WORDS = ['chief', 'steam', 'member', 'reason', 'speed', 'west', 'sleeve', 'believe', 'complete', 'fresh', 'pretend', 'cheap', 'greed', 'eager', 'gleam', 'beast', 'shelf', 'steep'];\n HINTS = { pretend: 'pretend — say the stressed part: -TEND. Short e.', believe: 'believe — same sound as sleeve, different letters. i before e.', sleeve: 'sleeve — same sound as believe, different letters.', complete: 'complete — the silent e at the end reaches back to make the e long.', member: 'member — say it: MEM. Short, like bed.', eager: 'eager — the long e is spelled ea, right at the front.' };\n B = [\n ['The fire', 'gave instructions to the firefighters.', 'chief'],\n ['I need to', 'my homework before dinner.', 'complete'],\n ['The stars began to', 'in the night sky.', 'gleam'],\n ['The mountain is very', 'and dangerous to climb.', 'steep'],\n ['The children like to', 'they are pirates.', 'pretend'],\n ['The students were', 'to go on their outing.', 'eager'],\n ];\n BANK = ['chief', 'steep', 'gleam', 'eager', 'complete', 'pretend'];\n componentDidMount() {\n this._move = (e) => {\n if (!this.state.drag) return;\n const d = this.state.drag;\n const moved = d.moved || Math.hypot(e.clientX - d.sx, e.clientY - d.sy) > 8;\n this.setState({ drag: { ...d, x: e.clientX, y: e.clientY, moved } });\n };\n this._up = (e) => {\n const d = this.state.drag;\n if (!d) return;\n if (!d.moved) { this.setState(p => ({ drag: null, sel: p.sel === d.word ? null : d.word })); return; }\n const el = document.elementFromPoint(e.clientX, e.clientY);\n const colEl = el && el.closest ? el.closest('[data-col]') : null;\n this.setState({ drag: null });\n if (colEl) this.tryPlace(d.word, colEl.getAttribute('data-col'));\n };\n window.addEventListener('pointermove', this._move);\n window.addEventListener('pointerup', this._up);\n }\n componentWillUnmount() { window.removeEventListener('pointermove', this._move); window.removeEventListener('pointerup', this._up); }\n tryPlace(word, colId) {\n if (this.state.placed[word]) return;\n if (this.ANSWER[word] === colId) {\n this.setState(p => ({ placed: { ...p.placed, [word]: colId }, sel: null, earned: p.earned + 1, hintWord: p.hintWord === word ? null : p.hintWord }));\n } else {\n this.setState(p => {\n const n = (p.tries[word] || 0) + 1;\n return { tries: { ...p.tries, [word]: n }, shakeCol: colId, shakeWord: word, hintWord: n >= 2 && this.HINTS[word] ? word : p.hintWord };\n });\n clearTimeout(this._ts); this._ts = setTimeout(() => this.setState({ shakeCol: null, shakeWord: null }), 450);\n }\n }\n shake(key, id) { this.setState({ [key]: id }); clearTimeout(this['_t' + key]); this['_t' + key] = setTimeout(() => this.setState({ [key]: null }), 450); }\n renderVals() {\n const s = this.state;\n const pool = this.WORDS.filter(w => !s.placed[w]).map(word => ({ word,\n bg: s.sel === word ? 'var(--color-accent-100)' : 'transparent',\n border: s.sel === word ? '1px solid var(--color-accent)' : '1px solid var(--color-divider)',\n anim: s.shakeWord === word ? 'omShake 0.4s' : 'none',\n down: (e) => { e.preventDefault(); this.setState({ drag: { word, x: e.clientX, y: e.clientY, sx: e.clientX, sy: e.clientY, moved: false } }); } }));\n const cols = this.COLS.map(c => {\n const placed = this.WORDS.filter(w => s.placed[w] === c.id).map(word => ({ word }));\n return { id: c.id, label: c.label, example: c.example, hex: c.hex, tint: c.tint, deep: c.deep,\n count: placed.length + ' of ' + c.total,\n placed,\n border: placed.length === c.total ? '1.5px solid ' + c.hex : '1px solid var(--color-divider)',\n anim: s.shakeCol === c.id ? 'omShake 0.4s' : 'none',\n tap: () => { if (s.sel) this.tryPlace(s.sel, c.id); } };\n });\n const usedB = {}; this.B.forEach(([,, ans], i) => { if (s.bDone[i]) usedB[ans] = true; });\n const bItems = this.B.map(([pre, post, ans], i) => {\n const done = !!s.bDone[i], sel = s.bSel === i;\n return { n: i + 1, pre, post,\n fill: done ? ans : '\\u00a0',\n bg: sel ? 'var(--color-accent-100)' : 'transparent',\n lineColor: done || sel ? 'var(--color-accent)' : 'var(--color-neutral-400)',\n anim: s.shakeB === i ? 'omShake 0.4s' : 'none',\n pick: () => { if (!done) this.setState({ bSel: sel ? null : i }); } };\n });\n const bank = this.BANK.map(word => ({ word,\n color: usedB[word] ? 'var(--color-neutral-400)' : 'var(--color-text)',\n deco: usedB[word] ? 'line-through' : 'none',\n pick: () => {\n const i = s.bSel;\n if (i == null || usedB[word]) return;\n if (this.B[i][2] === word) this.setState(p => ({ bDone: { ...p.bDone, [i]: true }, bSel: null, earned: p.earned + 1 }));\n else this.shake('shakeB', i);\n } }));\n return { earned: s.earned, teacher: s.teacher, explainA: s.explainA, explainB: s.explainB,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplainA: () => this.setState(p => ({ explainA: !p.explainA })),\n toggleExplainB: () => this.setState(p => ({ explainB: !p.explainB })),\n pool, cols, poolEmpty: pool.length === 0,\n dragging: !!(s.drag && s.drag.moved), dragWord: s.drag ? s.drag.word : '', dragX: s.drag ? s.drag.x : 0, dragY: s.drag ? s.drag.y : 0,\n hintWord: !!s.hintWord, hintText: s.hintWord ? this.HINTS[s.hintWord] : '',\n bItems, bank };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"2851637c-b0ad-44ef-b329-2a0ef7444c8f": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "a0a04b57-cc11-4168-998a-14604419ae48": {"mime": "image/png", "compressed": false, "data": "@@f67d5a848c0ea2@@"}, "6a192b48-1ebc-40c4-9b0b-283fceede1fd": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "beb8b4eb-6e2a-4a67-88b7-f28c2d2d1a25": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "b56469ba-b6cb-4f0c-9465-5205b0addf5a": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "b5b353a0-5d19-4b5f-b5c8-47b8a93ade7e": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "692bfe17-9ce9-455c-a43b-95b9b0949ac3": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "fa63cd59-9570-4c61-ba58-cc0ef00e483e": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "9a26031c-1133-4185-8ae8-ea9a1976e089": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "6394a627-4bfc-4c39-aa73-0d7237d5a36e": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "fcdf7707-1281-41e8-9a78-9df9058710d5": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "4c3e7c0e-f35d-42ae-a11c-f260b8eab9a1": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "37f00c1c-6d14-42d3-8ddf-bf631050dbda": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "4729651e-a607-4cb9-a175-f9cfb8658400": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "50932a91-e635-4fd1-bc0c-0347980c8017": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "e22213d8-1308-4894-8c3d-669a4c5ca504": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "08f52b12-236c-4fbe-a189-5cc9033f3ffd": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "171e29ed-005b-4088-9194-4eeaecebd072": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "e3dd5d9a-1f41-4477-8516-617755773295": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "1d1afa90-8c54-4271-b9f0-26c64eee176a": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "c49e5645-6388-4ca3-a7cd-0cb408244d62": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "ba1b31a1-2b4d-4021-9cec-3794383ad37b": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "8a8c3cee-61d1-4dc8-85e4-99a2bfa6f3b4": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "d4d0c5ec-c824-410e-8fd5-c16991e6e3b2": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "3d40f182-f8a4-4e06-98c0-4460d5ab3a41": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "67797486-a088-4fb1-b1ed-41cf1f61ed33": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f12c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f12c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f12c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f12c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"6a192b48-1ebc-40c4-9b0b-283fceede1fd\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b56469ba-b6cb-4f0c-9465-5205b0addf5a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b5b353a0-5d19-4b5f-b5c8-47b8a93ade7e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"692bfe17-9ce9-455c-a43b-95b9b0949ac3\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"fa63cd59-9570-4c61-ba58-cc0ef00e483e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"9a26031c-1133-4185-8ae8-ea9a1976e089\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b56469ba-b6cb-4f0c-9465-5205b0addf5a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b5b353a0-5d19-4b5f-b5c8-47b8a93ade7e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"692bfe17-9ce9-455c-a43b-95b9b0949ac3\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"fa63cd59-9570-4c61-ba58-cc0ef00e483e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"9a26031c-1133-4185-8ae8-ea9a1976e089\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6394a627-4bfc-4c39-aa73-0d7237d5a36e\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"fcdf7707-1281-41e8-9a78-9df9058710d5\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4c3e7c0e-f35d-42ae-a11c-f260b8eab9a1\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"37f00c1c-6d14-42d3-8ddf-bf631050dbda\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4729651e-a607-4cb9-a175-f9cfb8658400\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"50932a91-e635-4fd1-bc0c-0347980c8017\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e22213d8-1308-4894-8c3d-669a4c5ca504\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6394a627-4bfc-4c39-aa73-0d7237d5a36e\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"fcdf7707-1281-41e8-9a78-9df9058710d5\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4c3e7c0e-f35d-42ae-a11c-f260b8eab9a1\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"37f00c1c-6d14-42d3-8ddf-bf631050dbda\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4729651e-a607-4cb9-a175-f9cfb8658400\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"50932a91-e635-4fd1-bc0c-0347980c8017\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e22213d8-1308-4894-8c3d-669a4c5ca504\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"08f52b12-236c-4fbe-a189-5cc9033f3ffd\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"171e29ed-005b-4088-9194-4eeaecebd072\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e3dd5d9a-1f41-4477-8516-617755773295\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"1d1afa90-8c54-4271-b9f0-26c64eee176a\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c49e5645-6388-4ca3-a7cd-0cb408244d62\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"ba1b31a1-2b4d-4021-9cec-3794383ad37b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8a8c3cee-61d1-4dc8-85e4-99a2bfa6f3b4\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"08f52b12-236c-4fbe-a189-5cc9033f3ffd\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"171e29ed-005b-4088-9194-4eeaecebd072\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e3dd5d9a-1f41-4477-8516-617755773295\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"1d1afa90-8c54-4271-b9f0-26c64eee176a\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c49e5645-6388-4ca3-a7cd-0cb408244d62\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"ba1b31a1-2b4d-4021-9cec-3794383ad37b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8a8c3cee-61d1-4dc8-85e4-99a2bfa6f3b4\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"d4d0c5ec-c824-410e-8fd5-c16991e6e3b2\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3d40f182-f8a4-4e06-98c0-4460d5ab3a41\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"67797486-a088-4fb1-b1ed-41cf1f61ed33\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"beb8b4eb-6e2a-4a67-88b7-f28c2d2d1a25\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @keyframes omShake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Worksheet 13 · Writing — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">My Resolutions: “This Year I Will…”<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"2851637c-b0ad-44ef-b329-2a0ef7444c8f\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-style: italic; color: var(--color-neutral-600); margin: 0 0 var(--space-4); font-size: 19px;\">Before you start: read the whole story. You may also write these as Pacy, or as another character in the story — your choice.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher · watch for<\u002Fdiv>\n <p style=\"margin: 0 0 var(--space-2);\"><strong>State the opt-out to the whole class BEFORE anyone starts, once, neutrally:<\u002Fstrong> “If you would rather not write about yourself, you may write these as Pacy or as another character in the story.” No student should have to ask for it. Section 4 in particular can land badly for a child with a difficult adult at home — “any trusted adult, including a teacher or coach” is a fair widening.<\u002Fp>\n <p style=\"margin: 0;\">Support option: accept the frame plus any two of sections 2–5. Extension: one resolution as an exclamatory sentence and one as an imperative addressed to themselves. Deduct nothing for the checklist — use it to direct redrafting.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">✦<\u002Fspan>Five parts, written together<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">10 points · 2 per part<\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleExplain }}\" style=\"background: transparent; border: 1px solid var(--color-accent); border-radius: var(--radius-md); padding: 8px 16px; font-family: var(--font-body); font-size: 17px; color: var(--color-accent-700); cursor: pointer;\" style-hover=\"background: var(--color-accent-100);\">Explain<\u002Fbutton>\n <\u002Fdiv>\n <sc-if value=\"{{ explain }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Explanation guide<\u002Fdiv>\n <p style=\"margin: 0;\">Pacy makes resolutions at Chinese New Year — now the class makes theirs. The frame does the heavy lifting: an “I will” and an “I will not”, then a change, a cold door, an adviser, and one thing to do differently the way Pacy did with Becky. Checklist as you go: every sentence has a subject and a predicate · no fragments or run-ons · at least one exclamatory sentence.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n <div style=\"display: flex; flex-direction: column; gap: var(--space-4);\">\n <sc-for list=\"{{ parts }}\" as=\"p\" hint-placeholder-count=\"5\">\n <div style=\"border: 1px solid var(--color-divider); border-left: 4px solid {{ p.hex }}; border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);\">\n <div style=\"display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2);\">\n <span style=\"font-variant-numeric: tabular-nums; font-size: 19px; color: {{ p.deep }};\">{{ p.n }}<\u002Fspan>\n <span style=\"font-size: 22px;\">{{ p.label }}<\u002Fspan>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <button sc-camel-on-click=\"{{ p.award }}\" style=\"background: {{ p.awardBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 14px; font-size: 17px; font-family: var(--font-body); color: var(--color-text); cursor: pointer;\">{{ p.awardLabel }}<\u002Fbutton>\n <\u002Fdiv>\n <textarea rows=\"{{ p.rows }}\" placeholder=\"{{ p.ph }}\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.5; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <\u002Fdiv>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"a0a04b57-cc11-4168-998a-14604419ae48\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { awarded: {}, teacher: false, explain: false };\n PARTS = [\n ['1 \\u00b7', 'Start with the frame', 'This year I will\\u2026 / I will not\\u2026', 2, '#33517a', '#263d5c'],\n ['2 \\u00b7', 'One thing that changed for me this year', 'What changed?', 2, '#b5372f', '#8e2a24'],\n ['3 \\u00b7', 'My \\u201ccold door\\u201d \\u2014 something that looks hard, but I want to try it anyway', 'My cold door is\\u2026', 2, '#c9973f', '#7c5520'],\n ['4 \\u00b7', 'A person who gives me advice. What do they say or do?', 'Any trusted adult \\u2014 at home, at school, a coach\\u2026', 2, '#33517a', '#263d5c'],\n ['5 \\u00b7', 'One thing I will do differently, the way Pacy did with Becky', 'I will\\u2026', 2, '#b5372f', '#8e2a24'],\n ];\n renderVals() {\n const s = this.state;\n const parts = this.PARTS.map(([n, label, ph, rows, hex, deep], i) => {\n const awarded = !!s.awarded[i];\n return { n, label, ph, rows, hex, deep,\n awardLabel: awarded ? '\\u2713 2 pts' : 'Award 2 pts',\n awardBg: awarded ? 'var(--color-accent-100)' : 'transparent',\n award: () => this.setState(p => ({ awarded: { ...p.awarded, [i]: !awarded } })) };\n });\n return { earned: this.PARTS.reduce((a, _, i) => a + (s.awarded[i] ? 2 : 0), 0),\n teacher: s.teacher, explain: s.explain, parts,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })),\n toggleExplain: () => this.setState(p => ({ explain: !p.explain })) };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"1a0644d2-ffbc-4959-99f1-a7e42d357cb9": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "5e0aeeff-c62a-491d-8b1f-014a47a502b6": {"mime": "image/png", "compressed": false, "data": "@@f67d5a848c0ea2@@"}, "fe8206ab-1d8b-4db4-ab43-229e3b1ea208": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "6b2a42bb-2301-4738-a0cc-c88ca1c8f5ce": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "00509399-7fc8-4c3a-a65c-41fad5d5b1a5": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "89d2aad9-d42a-4de8-92ba-5f80d549ce62": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "fd7dab47-a7a8-4701-bb35-a6fe195b5970": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "cf3b3d98-f50f-4854-a5a8-044367ad4e8b": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "651fff95-053f-4354-b0dc-c2d75528eb51": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "f8c4c893-429a-498a-b944-7c9bd544d02d": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "86ecba33-4ee5-4953-ab09-6ce91d93a675": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "84bb3fe0-519e-47f3-a5c1-efd486cf4d39": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "b0e48535-05fb-4c5c-b7a1-62328a4bb284": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "d23b261a-df45-4c75-aefc-6bc9924a307d": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "6e97e354-a4d6-45a5-9bce-502baf47cb3d": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "5259947e-c87e-43a6-8320-d4c265262e5b": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "0576485f-7a4f-45b0-be0a-f55c7d21d029": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "2c3de56d-739f-426d-b848-1f089545c8da": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "8dcce421-a050-405f-92bb-f702bdbcd666": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "bc319e48-599d-454a-9e47-4f608f53b94d": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "6575fb22-680d-47d0-b253-1cc763eb0fbf": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "3cecd7ba-4d3e-4b56-a540-bfe4058b0e36": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "f3e69dd7-257a-4827-9426-b8feb2851cb5": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "8ffb1368-04f6-46ca-bb96-7c5974e329b6": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "f27b5cb0-b6b7-4c30-8865-5341327a3124": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "c22010f4-1577-49f4-9936-f2c80560dfa2": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f13c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f13c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f13c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f13c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"6b2a42bb-2301-4738-a0cc-c88ca1c8f5ce\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"00509399-7fc8-4c3a-a65c-41fad5d5b1a5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"89d2aad9-d42a-4de8-92ba-5f80d549ce62\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"fd7dab47-a7a8-4701-bb35-a6fe195b5970\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cf3b3d98-f50f-4854-a5a8-044367ad4e8b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"651fff95-053f-4354-b0dc-c2d75528eb51\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"00509399-7fc8-4c3a-a65c-41fad5d5b1a5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"89d2aad9-d42a-4de8-92ba-5f80d549ce62\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"fd7dab47-a7a8-4701-bb35-a6fe195b5970\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"cf3b3d98-f50f-4854-a5a8-044367ad4e8b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"651fff95-053f-4354-b0dc-c2d75528eb51\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f8c4c893-429a-498a-b944-7c9bd544d02d\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"86ecba33-4ee5-4953-ab09-6ce91d93a675\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"84bb3fe0-519e-47f3-a5c1-efd486cf4d39\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b0e48535-05fb-4c5c-b7a1-62328a4bb284\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d23b261a-df45-4c75-aefc-6bc9924a307d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6e97e354-a4d6-45a5-9bce-502baf47cb3d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5259947e-c87e-43a6-8320-d4c265262e5b\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f8c4c893-429a-498a-b944-7c9bd544d02d\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"86ecba33-4ee5-4953-ab09-6ce91d93a675\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"84bb3fe0-519e-47f3-a5c1-efd486cf4d39\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b0e48535-05fb-4c5c-b7a1-62328a4bb284\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d23b261a-df45-4c75-aefc-6bc9924a307d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6e97e354-a4d6-45a5-9bce-502baf47cb3d\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5259947e-c87e-43a6-8320-d4c265262e5b\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0576485f-7a4f-45b0-be0a-f55c7d21d029\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2c3de56d-739f-426d-b848-1f089545c8da\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8dcce421-a050-405f-92bb-f702bdbcd666\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"bc319e48-599d-454a-9e47-4f608f53b94d\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6575fb22-680d-47d0-b253-1cc763eb0fbf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3cecd7ba-4d3e-4b56-a540-bfe4058b0e36\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f3e69dd7-257a-4827-9426-b8feb2851cb5\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0576485f-7a4f-45b0-be0a-f55c7d21d029\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2c3de56d-739f-426d-b848-1f089545c8da\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8dcce421-a050-405f-92bb-f702bdbcd666\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"bc319e48-599d-454a-9e47-4f608f53b94d\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6575fb22-680d-47d0-b253-1cc763eb0fbf\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3cecd7ba-4d3e-4b56-a540-bfe4058b0e36\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f3e69dd7-257a-4827-9426-b8feb2851cb5\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"8ffb1368-04f6-46ca-bb96-7c5974e329b6\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f27b5cb0-b6b7-4c30-8865-5341327a3124\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"c22010f4-1577-49f4-9936-f2c80560dfa2\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"fe8206ab-1d8b-4db4-ab43-229e3b1ea208\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Free Writing 1 · pairs with Worksheet 8 — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">A Day That Changed Something<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"1a0644d2-ffbc-4959-99f1-a7e42d357cb9\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-size: 24px; margin: 0 0 var(--space-4);\"><strong>Write about a day when something changed for you.<\u002Fstrong> It can be a small change. Write 5–7 sentences — count your sentences, not your words.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher<\u002Fdiv>\n <p style=\"margin: 0;\">Rubric: Ideas 4 · Sentences 4 (one compound subject, one compound predicate) · Spelling & punctuation 2. After marking, students rewrite the corrected version neatly in their journals.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">1<\u002Fspan>Read the example · spot the toolbox<\u002Fh2>\n <\u002Fdiv>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); align-items: start;\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4); font-size: 24px; line-height: 1.7;\">\n Last winter my whole street lost power. <em>My mother and I<\u002Fem> lit candles in the kitchen. We could not watch television, so we told stories instead. My little brother <em>laughed and clapped<\u002Fem> at every one. The dark house felt warm and busy. That night changed how I think about a quiet evening.\n <\u002Fdiv>\n <div style=\"border: 1px solid var(--color-divider); border-left: 4px solid #33517a; border-radius: var(--radius-md); padding: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 15px; letter-spacing: 0.12em; text-transform: uppercase; color: #263d5c; margin-bottom: var(--space-2);\">Sentence toolbox · compound subjects & predicates<\u002Fdiv>\n A <strong>compound subject<\u002Fstrong> is two subjects sharing one predicate: <em>My mother and I lit candles.<\u002Fem><br>\n A <strong>compound predicate<\u002Fstrong> is one subject doing two things: <em>My brother laughed and clapped.<\u002Fem><br>\n Join them with <strong>and / or<\u002Fstrong> — do not start every sentence the same way.\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">2<\u002Fspan>Plan — words and phrases, not sentences yet<\u002Fh2>\n <\u002Fdiv>\n <ul style=\"font-size: 22px; line-height: 1.8; margin: 0 0 var(--space-4); padding-left: var(--space-6); color: var(--color-neutral-700);\">\n <li>What changed? Was it a place, a person, or a rule?<\u002Fli>\n <li>Who else was there? <em>(Try a compound subject here.)<\u002Fem><\u002Fli>\n <li>What did you do? <em>(Try a compound predicate here.)<\u002Fem><\u002Fli>\n <li>How did you feel at the start — and afterwards?<\u002Fli>\n <\u002Ful>\n <textarea rows=\"4\" placeholder=\"Jot the class plan here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">3<\u002Fspan>Write it — then mark it<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">5–7 sentences · about 55–75 words<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin: 0 0 var(--space-3);\">Challenge: use at least one compound subject and one compound predicate — and underline them when you read it back.<\u002Fp>\n <textarea rows=\"7\" placeholder=\"My title… then the paragraph, written together…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 24px; line-height: 1.7; padding: var(--space-4); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical; margin-bottom: var(--space-4);\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-6); flex-wrap: wrap; border-top: 1px solid var(--color-divider); padding-top: var(--space-4); align-items: center;\">\n <sc-for list=\"{{ rubric }}\" as=\"r\" hint-placeholder-count=\"3\">\n <div style=\"display: flex; gap: var(--space-2); align-items: center;\">\n <span style=\"font-size: 19px; color: var(--color-neutral-700);\">{{ r.label }}<\u002Fspan>\n <sc-for list=\"{{ r.pts }}\" as=\"p\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ p.pick }}\" style=\"width: 44px; height: 44px; border-radius: 50%; font-family: var(--font-body); font-size: 20px; font-variant-numeric: tabular-nums; border: 1.5px solid {{ p.border }}; background: {{ p.bg }}; color: {{ p.color }}; cursor: pointer;\">{{ p.n }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsc-for>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <span style=\"font-size: 19px; font-style: italic; color: var(--color-neutral-600);\">Then rewrite the corrected version neatly in your journal.<\u002Fspan>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"5e0aeeff-c62a-491d-8b1f-014a47a502b6\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { awarded: { 0: 0, 1: 0, 2: 0 }, teacher: false };\n RUBRIC = [['Ideas', 4], ['Sentences', 4], ['Spelling & punctuation', 2]];\n renderVals() {\n const s = this.state;\n const rubric = this.RUBRIC.map(([label, max], ri) => {\n const cur = s.awarded[ri] || 0;\n return { label, pts: Array.from({ length: max }, (_, k) => k + 1).map(n => ({ n,\n bg: n <= cur ? 'var(--color-accent-100)' : 'transparent',\n border: n <= cur ? 'var(--color-accent)' : 'var(--color-divider)',\n color: n <= cur ? 'var(--color-accent-700)' : 'var(--color-neutral-600)',\n pick: () => this.setState(p => ({ awarded: { ...p.awarded, [ri]: (p.awarded[ri] === n ? 0 : n) } })) })) };\n });\n return { earned: (s.awarded[0] || 0) + (s.awarded[1] || 0) + (s.awarded[2] || 0),\n teacher: s.teacher, rubric,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })) };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"94ca9ebe-ef4e-4b28-a9ca-a4ecbff5666d": {"mime": "image/jpeg", "compressed": false, "data": "@@c9e3157db338b3@@"}, "c7c3e90a-6f11-48d8-83df-11082b3ea8d6": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "0614e7cb-7ed9-49a0-8489-19caa321a2da": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "47d0db6b-d63c-45dd-9109-2c627000207a": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "c8ca2515-b087-4682-a9da-ebc52e803792": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "51d210d5-670e-4908-9391-8da442d4f513": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "1ce4d174-3ceb-4cf4-8784-77513c9244fd": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "cca502f9-f2a6-4736-b624-e8b18604fea1": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "fda3d159-4e7c-4a7a-9862-d526a6e4a7fa": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "300784b4-fb87-4c97-9823-ab7adeef49d5": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "0d5e6473-36dc-4264-a243-7e686c4a333e": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "c58dbbf9-37ee-4a95-b0c5-42bd63688b4c": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "b42cfe16-c9a6-43f3-9f66-7c5f769d9e7d": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "f1896391-52fe-4827-af20-984f8a947cf3": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "a973a059-ec5b-4d52-8a0a-d61d6f51f71e": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "91a67e5b-d8d9-445f-8f80-e02561a27820": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "27761448-51ce-414e-9c31-5eb97b843f1e": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "8fa9e9aa-5f72-4960-9f9c-06e95ebc39f9": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "d3618451-fc47-4aeb-a23a-26e5e122d1f8": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "1fc023fe-b6ff-4105-b66b-54edf7809d4a": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "420c10f0-968e-402b-9bcb-c03d9a8f0c51": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "df915d55-30f1-47c7-8e71-5f4c55a831f9": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "15d29930-b976-40ac-88f1-278a20491206": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "2f09fc22-44eb-4dd8-9845-bf894e7cbb6c": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "d7ab753f-3632-4bd2-9299-e43545c17f25": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "79305495-5fa1-43d3-bcec-d5558ef1c430": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f14c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f14c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f14c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f14c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"47d0db6b-d63c-45dd-9109-2c627000207a\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c8ca2515-b087-4682-a9da-ebc52e803792\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"51d210d5-670e-4908-9391-8da442d4f513\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"1ce4d174-3ceb-4cf4-8784-77513c9244fd\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cca502f9-f2a6-4736-b624-e8b18604fea1\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"fda3d159-4e7c-4a7a-9862-d526a6e4a7fa\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c8ca2515-b087-4682-a9da-ebc52e803792\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"51d210d5-670e-4908-9391-8da442d4f513\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"1ce4d174-3ceb-4cf4-8784-77513c9244fd\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"cca502f9-f2a6-4736-b624-e8b18604fea1\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"fda3d159-4e7c-4a7a-9862-d526a6e4a7fa\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"300784b4-fb87-4c97-9823-ab7adeef49d5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0d5e6473-36dc-4264-a243-7e686c4a333e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c58dbbf9-37ee-4a95-b0c5-42bd63688b4c\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b42cfe16-c9a6-43f3-9f66-7c5f769d9e7d\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"f1896391-52fe-4827-af20-984f8a947cf3\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a973a059-ec5b-4d52-8a0a-d61d6f51f71e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"91a67e5b-d8d9-445f-8f80-e02561a27820\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"300784b4-fb87-4c97-9823-ab7adeef49d5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0d5e6473-36dc-4264-a243-7e686c4a333e\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c58dbbf9-37ee-4a95-b0c5-42bd63688b4c\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b42cfe16-c9a6-43f3-9f66-7c5f769d9e7d\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"f1896391-52fe-4827-af20-984f8a947cf3\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a973a059-ec5b-4d52-8a0a-d61d6f51f71e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"91a67e5b-d8d9-445f-8f80-e02561a27820\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"27761448-51ce-414e-9c31-5eb97b843f1e\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"8fa9e9aa-5f72-4960-9f9c-06e95ebc39f9\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d3618451-fc47-4aeb-a23a-26e5e122d1f8\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"1fc023fe-b6ff-4105-b66b-54edf7809d4a\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"420c10f0-968e-402b-9bcb-c03d9a8f0c51\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"df915d55-30f1-47c7-8e71-5f4c55a831f9\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"15d29930-b976-40ac-88f1-278a20491206\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"27761448-51ce-414e-9c31-5eb97b843f1e\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"8fa9e9aa-5f72-4960-9f9c-06e95ebc39f9\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d3618451-fc47-4aeb-a23a-26e5e122d1f8\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"1fc023fe-b6ff-4105-b66b-54edf7809d4a\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"420c10f0-968e-402b-9bcb-c03d9a8f0c51\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"df915d55-30f1-47c7-8e71-5f4c55a831f9\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"15d29930-b976-40ac-88f1-278a20491206\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"2f09fc22-44eb-4dd8-9845-bf894e7cbb6c\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d7ab753f-3632-4bd2-9299-e43545c17f25\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"79305495-5fa1-43d3-bcec-d5558ef1c430\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"0614e7cb-7ed9-49a0-8489-19caa321a2da\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Free Writing 2 · pairs with Worksheet 9 — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">Something I Like to Watch<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"c7c3e90a-6f11-48d8-83df-11082b3ea8d6\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-size: 24px; margin: 0 0 var(--space-4);\"><strong>What kind of sports or other events do you like to watch?<\u002Fstrong> Write a paragraph about it — 5–7 sentences; count your sentences, not your words.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher<\u002Fdiv>\n <p style=\"margin: 0;\">From Grammar 1.1.5 · Connect to Writing · subject–verb agreement. Rubric: Ideas 4 · Sentences 4 (agreement) · Spelling & punctuation 2 — then rewrite the corrected version in the journal.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">1<\u002Fspan>Read the example · spot the toolbox<\u002Fh2>\n <\u002Fdiv>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); align-items: start;\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4); font-size: 24px; line-height: 1.7;\">\n I love watching badminton with my father. The players <em>move<\u002Fem> so fast that the shuttlecock almost disappears. My father <em>explains<\u002Fem> every rule, and he never gets tired of my questions. My sister thinks the game is boring, but she still sits with us. We eat guava slices during the long matches. Badminton is the one hour when my whole family stops rushing.\n <\u002Fdiv>\n <div style=\"border: 1px solid var(--color-divider); border-left: 4px solid #33517a; border-radius: var(--radius-md); padding: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 15px; letter-spacing: 0.12em; text-transform: uppercase; color: #263d5c; margin-bottom: var(--space-2);\">Sentence toolbox · subject–verb agreement<\u002Fdiv>\n One person or thing → add <strong>-s<\u002Fstrong>: <em>The player moves fast.<\u002Fem><br>\n More than one → no -s: <em>The players move fast.<\u002Fem><br>\n Careful: <strong>he, she<\u002Fstrong> and <strong>it<\u002Fstrong> always take the -s form.\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">2<\u002Fspan>Plan — words and phrases, not sentences yet<\u002Fh2>\n <\u002Fdiv>\n <ul style=\"font-size: 22px; line-height: 1.8; margin: 0 0 var(--space-4); padding-left: var(--space-6); color: var(--color-neutral-700);\">\n <li>What do you like to watch? A sport, a show, a festival, a competition?<\u002Fli>\n <li>Who do you watch it with?<\u002Fli>\n <li>What happens that makes it exciting?<\u002Fli>\n <li>Why does it matter to you?<\u002Fli>\n <\u002Ful>\n <textarea rows=\"4\" placeholder=\"Jot the class plan here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">3<\u002Fspan>Write it — then mark it<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">5–7 sentences · about 60–80 words<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin: 0 0 var(--space-3);\">Challenge: circle three present-tense verbs, then check each one against its subject.<\u002Fp>\n <textarea rows=\"7\" placeholder=\"My title… then the paragraph, written together…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 24px; line-height: 1.7; padding: var(--space-4); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical; margin-bottom: var(--space-4);\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-6); flex-wrap: wrap; border-top: 1px solid var(--color-divider); padding-top: var(--space-4); align-items: center;\">\n <sc-for list=\"{{ rubric }}\" as=\"r\" hint-placeholder-count=\"3\">\n <div style=\"display: flex; gap: var(--space-2); align-items: center;\">\n <span style=\"font-size: 19px; color: var(--color-neutral-700);\">{{ r.label }}<\u002Fspan>\n <sc-for list=\"{{ r.pts }}\" as=\"p\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ p.pick }}\" style=\"width: 44px; height: 44px; border-radius: 50%; font-family: var(--font-body); font-size: 20px; font-variant-numeric: tabular-nums; border: 1.5px solid {{ p.border }}; background: {{ p.bg }}; color: {{ p.color }}; cursor: pointer;\">{{ p.n }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsc-for>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <span style=\"font-size: 19px; font-style: italic; color: var(--color-neutral-600);\">Then rewrite the corrected version neatly in your journal.<\u002Fspan>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"94ca9ebe-ef4e-4b28-a9ca-a4ecbff5666d\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 150px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { awarded: { 0: 0, 1: 0, 2: 0 }, teacher: false };\n RUBRIC = [['Ideas', 4], ['Sentences', 4], ['Spelling & punctuation', 2]];\n renderVals() {\n const s = this.state;\n const rubric = this.RUBRIC.map(([label, max], ri) => {\n const cur = s.awarded[ri] || 0;\n return { label, pts: Array.from({ length: max }, (_, k) => k + 1).map(n => ({ n,\n bg: n <= cur ? 'var(--color-accent-100)' : 'transparent',\n border: n <= cur ? 'var(--color-accent)' : 'var(--color-divider)',\n color: n <= cur ? 'var(--color-accent-700)' : 'var(--color-neutral-600)',\n pick: () => this.setState(p => ({ awarded: { ...p.awarded, [ri]: (p.awarded[ri] === n ? 0 : n) } })) })) };\n });\n return { earned: (s.awarded[0] || 0) + (s.awarded[1] || 0) + (s.awarded[2] || 0),\n teacher: s.teacher, rubric,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })) };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"00242d00-a3b0-43ad-bfcc-8e877fb4c7fb": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "e00378c5-2122-4f5e-9d30-f57a1c6aff15": {"mime": "image/png", "compressed": false, "data": "@@f328acdee21da3@@"}, "6e5d9be9-afba-49f7-be4d-353b9704eae2": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "ec191e3c-2116-48aa-aa08-db2ef1d9f522": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "feb1f304-58d3-4076-8f2a-79abab452075": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "e6737673-dadf-40e2-a10f-a5592e0d1831": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "48487a90-62d7-4c18-88c4-4317df52157c": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "b3b7b2f7-5bed-4ed0-b89b-ca2b0e2ce11e": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "c0cffc12-b83e-4fc5-82e9-28d2142d86dd": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "07e6ba77-1aad-4b0b-b2b4-4a4f402cd865": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "e935877a-c8d8-45e7-be3d-d136d7e8aef9": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "fb000dd4-1cb0-49b2-aaaa-af86a9c853e9": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "63a3b988-fd04-4d6f-a921-fc16455f6077": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "2a564e21-b6e9-4cae-8b65-1596e16be61f": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "d4960d65-913b-412e-8b5b-9aaa44bc7f71": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "3ce8c28c-720c-4a8b-855b-c58c9cb3bf0f": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "eabc7b43-dbc9-4307-8218-e1af5d70632a": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "43338e75-28b6-4373-ac39-7f2dadd67555": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "efa5c717-c600-4f67-bc18-c6bc8e357ca4": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "91ac1d4f-8a60-4eee-bcff-ca201e196f16": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "5a6f2bcb-84da-4ae2-98bc-464c3e01da7b": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "929c6a06-9870-4398-8fd0-8603a7486619": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "a0943fa2-ebf2-4ed0-8a7a-2adefee1b834": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "369a6049-8388-4f73-b073-945e3fa7b396": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "4fb44bec-1f1b-4d12-8ead-ac14e01b4f42": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "1dc0e609-eb1a-47ee-97cd-9eee8c763834": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f15c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f15c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f15c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f15c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"ec191e3c-2116-48aa-aa08-db2ef1d9f522\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"feb1f304-58d3-4076-8f2a-79abab452075\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e6737673-dadf-40e2-a10f-a5592e0d1831\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"48487a90-62d7-4c18-88c4-4317df52157c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b3b7b2f7-5bed-4ed0-b89b-ca2b0e2ce11e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"c0cffc12-b83e-4fc5-82e9-28d2142d86dd\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"feb1f304-58d3-4076-8f2a-79abab452075\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e6737673-dadf-40e2-a10f-a5592e0d1831\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"48487a90-62d7-4c18-88c4-4317df52157c\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b3b7b2f7-5bed-4ed0-b89b-ca2b0e2ce11e\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"c0cffc12-b83e-4fc5-82e9-28d2142d86dd\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"07e6ba77-1aad-4b0b-b2b4-4a4f402cd865\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"e935877a-c8d8-45e7-be3d-d136d7e8aef9\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"fb000dd4-1cb0-49b2-aaaa-af86a9c853e9\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"63a3b988-fd04-4d6f-a921-fc16455f6077\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"2a564e21-b6e9-4cae-8b65-1596e16be61f\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d4960d65-913b-412e-8b5b-9aaa44bc7f71\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3ce8c28c-720c-4a8b-855b-c58c9cb3bf0f\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"07e6ba77-1aad-4b0b-b2b4-4a4f402cd865\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"e935877a-c8d8-45e7-be3d-d136d7e8aef9\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"fb000dd4-1cb0-49b2-aaaa-af86a9c853e9\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"63a3b988-fd04-4d6f-a921-fc16455f6077\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"2a564e21-b6e9-4cae-8b65-1596e16be61f\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d4960d65-913b-412e-8b5b-9aaa44bc7f71\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3ce8c28c-720c-4a8b-855b-c58c9cb3bf0f\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"eabc7b43-dbc9-4307-8218-e1af5d70632a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"43338e75-28b6-4373-ac39-7f2dadd67555\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"efa5c717-c600-4f67-bc18-c6bc8e357ca4\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"91ac1d4f-8a60-4eee-bcff-ca201e196f16\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5a6f2bcb-84da-4ae2-98bc-464c3e01da7b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"929c6a06-9870-4398-8fd0-8603a7486619\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a0943fa2-ebf2-4ed0-8a7a-2adefee1b834\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"eabc7b43-dbc9-4307-8218-e1af5d70632a\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"43338e75-28b6-4373-ac39-7f2dadd67555\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"efa5c717-c600-4f67-bc18-c6bc8e357ca4\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"91ac1d4f-8a60-4eee-bcff-ca201e196f16\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5a6f2bcb-84da-4ae2-98bc-464c3e01da7b\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"929c6a06-9870-4398-8fd0-8603a7486619\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a0943fa2-ebf2-4ed0-8a7a-2adefee1b834\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"369a6049-8388-4f73-b073-945e3fa7b396\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4fb44bec-1f1b-4d12-8ead-ac14e01b4f42\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"1dc0e609-eb1a-47ee-97cd-9eee8c763834\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"6e5d9be9-afba-49f7-be4d-353b9704eae2\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Free Writing 3 · pairs with Worksheet 10 — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">A Birthday I Remember<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"00242d00-a3b0-43ad-bfcc-8e877fb4c7fb\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-size: 24px; margin: 0 0 var(--space-4);\"><strong>Write about a birthday you remember.<\u002Fstrong> Be sure to include each type of sentence in your writing — 6–8 sentences; count your sentences, not your words.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher<\u002Fdiv>\n <p style=\"margin: 0;\">From Grammar 1.2.5 · Connect to Writing · four kinds of sentences. Rubric: Ideas 4 · Sentences 4 (all four kinds used) · Spelling & punctuation 2 — then rewrite the corrected version in the journal.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">1<\u002Fspan>Read the example · find all four kinds<\u002Fh2>\n <\u002Fdiv>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); align-items: start;\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4); font-size: 24px; line-height: 1.7;\">\n My eighth birthday started badly. <em>Have you ever waited all morning for guests who never came?<\u002Fem> The rain was too heavy for anyone to ride a bicycle. Then my grandmother knocked on the door with a huge pineapple cake. <em>I could not believe it!<\u002Fem> <em>Ask me about that cake one day.<\u002Fem> It turned into the best birthday I have ever had.\n <\u002Fdiv>\n <div style=\"border: 1px solid var(--color-divider); border-left: 4px solid #33517a; border-radius: var(--radius-md); padding: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 15px; letter-spacing: 0.12em; text-transform: uppercase; color: #263d5c; margin-bottom: var(--space-2);\">Sentence toolbox · all four kinds<\u002Fdiv>\n <strong>Declarative<\u002Fstrong> tells something. (.) <strong>Interrogative<\u002Fstrong> asks. (?)<br>\n <strong>Imperative<\u002Fstrong> gives a command. (.) <strong>Exclamatory<\u002Fstrong> shows strong feeling. (!)<br>\n The example uses all four — find them.\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">2<\u002Fspan>Plan — words and phrases, not sentences yet<\u002Fh2>\n <\u002Fdiv>\n <ul style=\"font-size: 22px; line-height: 1.8; margin: 0 0 var(--space-4); padding-left: var(--space-6); color: var(--color-neutral-700);\">\n <li>Whose birthday was it? How old were you?<\u002Fli>\n <li>What went wrong, or what surprised you?<\u002Fli>\n <li>Who was there? What did somebody say?<\u002Fli>\n <li>How did the day end?<\u002Fli>\n <\u002Ful>\n <textarea rows=\"4\" placeholder=\"Jot the class plan here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">3<\u002Fspan>Write it — then mark it<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">6–8 sentences · about 65–85 words<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin: 0 0 var(--space-3);\">Challenge: use all four kinds of sentences, and label each one D, INT, IMP or E in the margin.<\u002Fp>\n <textarea rows=\"7\" placeholder=\"My title… then the paragraph, written together…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 24px; line-height: 1.7; padding: var(--space-4); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical; margin-bottom: var(--space-4);\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-6); flex-wrap: wrap; border-top: 1px solid var(--color-divider); padding-top: var(--space-4); align-items: center;\">\n <sc-for list=\"{{ rubric }}\" as=\"r\" hint-placeholder-count=\"3\">\n <div style=\"display: flex; gap: var(--space-2); align-items: center;\">\n <span style=\"font-size: 19px; color: var(--color-neutral-700);\">{{ r.label }}<\u002Fspan>\n <sc-for list=\"{{ r.pts }}\" as=\"p\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ p.pick }}\" style=\"width: 44px; height: 44px; border-radius: 50%; font-family: var(--font-body); font-size: 20px; font-variant-numeric: tabular-nums; border: 1.5px solid {{ p.border }}; background: {{ p.bg }}; color: {{ p.color }}; cursor: pointer;\">{{ p.n }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsc-for>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <span style=\"font-size: 19px; font-style: italic; color: var(--color-neutral-600);\">Then rewrite the corrected version neatly in your journal.<\u002Fspan>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"e00378c5-2122-4f5e-9d30-f57a1c6aff15\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 120px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { awarded: { 0: 0, 1: 0, 2: 0 }, teacher: false };\n RUBRIC = [['Ideas', 4], ['Sentences', 4], ['Spelling & punctuation', 2]];\n renderVals() {\n const s = this.state;\n const rubric = this.RUBRIC.map(([label, max], ri) => {\n const cur = s.awarded[ri] || 0;\n return { label, pts: Array.from({ length: max }, (_, k) => k + 1).map(n => ({ n,\n bg: n <= cur ? 'var(--color-accent-100)' : 'transparent',\n border: n <= cur ? 'var(--color-accent)' : 'var(--color-divider)',\n color: n <= cur ? 'var(--color-accent-700)' : 'var(--color-neutral-600)',\n pick: () => this.setState(p => ({ awarded: { ...p.awarded, [ri]: (p.awarded[ri] === n ? 0 : n) } })) })) };\n });\n return { earned: (s.awarded[0] || 0) + (s.awarded[1] || 0) + (s.awarded[2] || 0),\n teacher: s.teacher, rubric,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })) };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bundled Page</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f3f2f2; display: flex; align-items: center; justify-content: center; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
#__bundler_loading { position: fixed; bottom: 20px; right: 20px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif; color: #666; background: #fff; padding: 8px 14px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.12); z-index: 10000; }
#__bundler_thumbnail { position: fixed; inset: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f3f2f2; z-index: 9999; }
#__bundler_thumbnail svg { width: 100%; height: 100%; object-fit: contain; }
#__bundler_placeholder { color: #999; font-size: 14px; }
</style>
<noscript>
<style>#__bundler_loading { display: none; }</style>
<div style="position:fixed;bottom:12px;left:12px;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;color:#999;background:rgba(255,255,255,0.9);padding:6px 12px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.08);z-index:10000;">
This page requires JavaScript to display.
</div>
</noscript>
</head>
<body>
<div id="__bundler_thumbnail">
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#f3f2f2"></rect>
<rect x="0" y="0" width="400" height="12" fill="#33517a"></rect><rect x="400" y="0" width="400" height="12" fill="#b5372f"></rect><rect x="800" y="0" width="400" height="12" fill="#c9973f"></rect>
<circle cx="600" cy="400" r="180" fill="none" stroke="#b5372f" stroke-width="14"></circle>
<text x="600" y="460" text-anchor="middle" font-family="Georgia, serif" font-size="170" fill="#201f1d">鼠</text>
</svg>
</div>
<div id="__bundler_loading">Unpacking...</div>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const loading = document.getElementById('__bundler_loading');
function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#<uuid> marker
// instead of a substitutable uuid. Every document mints the blobs for
// its OWN frames — from local text at the root, or text obtained from
// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#<uuid>
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '<script>window.__resources = ' +
JSON.stringify(resourceMap).replace(/<\//g, '<\\/') +
';</' + 'script>';
// Inject after <head> so the DOCTYPE stays first; prepending the script
// would push the parser into quirks mode. DOMParser always emits a <head>
// (synthesizing one if the source HTML omitted it) but may carry
// attributes through, so match the full opening tag. slice() rather than
// replace() keeps us clear of $-pattern substitution in resourceScript.
const headOpen = template.match(/<head[^>]*>/i);
if (headOpen) {
const i = headOpen.index + headOpen[0].length;
template = template.slice(0, i) + resourceScript + template.slice(i);
}
// Parse the template and swap the root element. Scripts inserted via
// DOMParser/replaceWith are inert per spec — re-create each with
// createElement so they execute, awaiting onload for src scripts to
// preserve ordering (React before ReactDOM before Babel before text/babel).
const doc = new DOMParser().parseFromString(template, 'text/html');
document.documentElement.replaceWith(doc.documentElement);
// Resolve page-frame markers: mint this document's own blob for each
// framed page — from local text when this is the root, otherwise by
// asking the direct parent over the relay protocol above. An
// unanswered request leaves the frame on about:blank, the same
// degraded state as a refused embed.
{
const MARKER = /^about:blank#([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(#.*)?$/;
const needed = [];
for (const el of Array.from(document.querySelectorAll('iframe'))) {
const m = MARKER.exec(el.getAttribute('src') || '');
if (!m) continue;
const text = pageTexts[m[1]];
if (typeof text === 'string') {
mountPage(el, m[2] || '', text);
} else if (window.parent !== window && !isPageRoot) {
const had = pendingFrames[m[1]];
(pendingFrames[m[1]] = had || []).push({ el: el, frag: m[2] || '' });
if (!had) needed.push(m[1]);
}
}
for (const uuid of needed) {
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
}
}
const dead = Array.from(document.scripts);
for (const old of dead) {
const s = document.createElement('script');
for (const a of old.attributes) s.setAttribute(a.name, a.value);
s.textContent = old.textContent;
// text/babel scripts with a src: read and inline. transformScriptTags
// does XHR against the src, but blob:null/ from a file:// origin is
// silently dropped. Inlining makes it a plain inline babel script,
// which transformScriptTags handles unconditionally.
if ((s.type === 'text/babel' || s.type === 'text/jsx') && s.src) {
const pre = resourceBlobs[s.src.split('#')[0]];
s.textContent = pre ? await pre.text() : await (await fetch(s.src)).text();
s.removeAttribute('src');
}
const p = s.src ? new Promise(function(r) { s.onload = s.onerror = r; }) : null;
old.replaceWith(s);
if (p) await p;
}
// Babel standalone auto-transforms type=text/babel on DOMContentLoaded,
// which fired before we swapped the document. Trigger manually if present.
if (window.Babel && typeof window.Babel.transformScriptTags === 'function') {
window.Babel.transformScriptTags();
}
} catch (err) {
setStatus('Error unpacking: ' + err.message);
console.error('Bundle unpack error:', err);
}
});
</script>
<script type="__bundler/manifest">{"850f90bc-7b0d-4fd3-bc3f-7565ec59809a": {"mime": "image/png", "compressed": false, "data": "@@22ece29af9e9bb@@"}, "16f1198d-663f-4528-9efb-063a5d57f9ba": {"mime": "image/jpeg", "compressed": false, "data": "@@4346f924f586c6@@"}, "016285e6-4d1c-400d-ba40-6a39311de7f0": {"mime": "application/javascript", "compressed": true, "data": "@@6ebc90d2f9a5ec@@"}, "46230734-5ccf-49ea-97ae-32aeef339d12": {"mime": "text/javascript", "compressed": true, "data": "@@ce132622edad86@@"}, "cc34507d-e2ca-4178-ad5b-e7f26727e8d5": {"mime": "font/woff2", "compressed": false, "data": "@@66ef3e1440d93f@@"}, "6df24ed5-0b05-4aee-9714-66fe0fa4b220": {"mime": "font/woff2", "compressed": false, "data": "@@ad02fa047a24fe@@"}, "3f93d400-8560-420f-b56c-7cbfa80121b9": {"mime": "font/woff2", "compressed": false, "data": "@@0d438810ea2ee1@@"}, "3977fa6a-36c4-49ea-aef0-ff51b651df95": {"mime": "font/woff2", "compressed": false, "data": "@@3e347e29a07c27@@"}, "01087ebd-9fc7-42e5-a99b-64f1b08e6ebe": {"mime": "font/woff2", "compressed": false, "data": "@@6cc6864793a779@@"}, "4dca5662-b50d-453c-b81e-2044f29e3203": {"mime": "font/woff2", "compressed": false, "data": "@@80a48ed49e6b1d@@"}, "a5cd086a-7458-4e0e-a5ec-c73d6865969a": {"mime": "font/woff2", "compressed": false, "data": "@@fc07f011cdad0a@@"}, "b7667cd0-1be6-48e6-bdf2-7d22dd1aeaba": {"mime": "font/woff2", "compressed": false, "data": "@@4009fc09c81dc8@@"}, "861bb742-fecc-4814-aea4-c08065e6f024": {"mime": "font/woff2", "compressed": false, "data": "@@c7af5292e241fe@@"}, "0d382f77-f162-44ce-b3b3-f79e79609965": {"mime": "font/woff2", "compressed": false, "data": "@@4e0670e7a62e39@@"}, "d71fb628-ff97-40de-8178-1b51244c312b": {"mime": "font/woff2", "compressed": false, "data": "@@d207b367817d5e@@"}, "a46706f9-d904-44ed-bc4a-e899edc72861": {"mime": "font/woff2", "compressed": false, "data": "@@2dfc2ccbeec531@@"}, "6c1930a1-55c7-45db-95c3-623f1459f9be": {"mime": "font/woff2", "compressed": false, "data": "@@b860fc0910245b@@"}, "5bd07e81-1765-4b18-808c-c34c56467c93": {"mime": "font/woff2", "compressed": false, "data": "@@aff406ef1475a8@@"}, "397cab2d-66ac-4207-a7dc-c1a1b688a5b9": {"mime": "font/woff2", "compressed": false, "data": "@@8cb23cb94676e9@@"}, "92995646-7006-40e7-8fbf-11c17bb73cf1": {"mime": "font/woff2", "compressed": false, "data": "@@fbd58f7ff684b8@@"}, "3fc6f1f0-669d-440f-8c89-d7c4cc55921d": {"mime": "font/woff2", "compressed": false, "data": "@@4a1f898ad693d2@@"}, "534cf078-ef38-480f-851e-74b9e7073514": {"mime": "font/woff2", "compressed": false, "data": "@@b5228501ada012@@"}, "fefee7c2-5020-47d6-99cb-43c48ae4a77d": {"mime": "font/woff2", "compressed": false, "data": "@@569ce6de40eac4@@"}, "baf7348b-eb32-43ea-a01f-e67d852a2856": {"mime": "font/woff2", "compressed": false, "data": "@@f94c3553355532@@"}, "4b162550-3730-49f5-94e0-5e5d1133e3b6": {"mime": "font/woff2", "compressed": false, "data": "@@f0c48eeb766a26@@"}, "6c43c3db-41c1-466a-ab0a-0589aceca239": {"mime": "font/woff2", "compressed": false, "data": "@@1916d3cc9595ac@@"}, "f16c0de0-0000-4000-8000-000000000000": {"mime": "text/javascript", "compressed": true, "data": "@@0bacd0de1d3d03@@"}, "f16c0de0-0000-4000-8000-000000000001": {"mime": "text/javascript", "compressed": true, "data": "@@62749b9269f673@@"}}</script>
<script type="__bundler/ext_resources">[{"id": "https://unpkg.com/react@18.3.1/umd/react.production.min.js", "uuid": "f16c0de0-0000-4000-8000-000000000000"}, {"id": "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js", "uuid": "f16c0de0-0000-4000-8000-000000000001"}]</script>
<script type="__bundler/page_order">
[]
</script>
<script type="__bundler/template">
"<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<script src=\"46230734-5ccf-49ea-97ae-32aeef339d12\"><\u002Fscript>\n\n\n<\u002Fhead>\n<body>\n<x-dc>\n<helmet>\n <style>/* Classical — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"cc34507d-e2ca-4178-ad5b-e7f26727e8d5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6df24ed5-0b05-4aee-9714-66fe0fa4b220\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3f93d400-8560-420f-b56c-7cbfa80121b9\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3977fa6a-36c4-49ea-aef0-ff51b651df95\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"01087ebd-9fc7-42e5-a99b-64f1b08e6ebe\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"cc34507d-e2ca-4178-ad5b-e7f26727e8d5\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6df24ed5-0b05-4aee-9714-66fe0fa4b220\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3f93d400-8560-420f-b56c-7cbfa80121b9\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3977fa6a-36c4-49ea-aef0-ff51b651df95\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Cormorant Garamond';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"01087ebd-9fc7-42e5-a99b-64f1b08e6ebe\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4dca5662-b50d-453c-b81e-2044f29e3203\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a5cd086a-7458-4e0e-a5ec-c73d6865969a\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"b7667cd0-1be6-48e6-bdf2-7d22dd1aeaba\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"861bb742-fecc-4814-aea4-c08065e6f024\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"0d382f77-f162-44ce-b3b3-f79e79609965\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"d71fb628-ff97-40de-8178-1b51244c312b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"a46706f9-d904-44ed-bc4a-e899edc72861\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"4dca5662-b50d-453c-b81e-2044f29e3203\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a5cd086a-7458-4e0e-a5ec-c73d6865969a\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* math */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"b7667cd0-1be6-48e6-bdf2-7d22dd1aeaba\") format('woff2');\n unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;\n}\n/* symbols */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"861bb742-fecc-4814-aea4-c08065e6f024\") format('woff2');\n unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"0d382f77-f162-44ce-b3b3-f79e79609965\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"d71fb628-ff97-40de-8178-1b51244c312b\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Lora';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"a46706f9-d904-44ed-bc4a-e899edc72861\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n\n:root {\n --color-bg: #f3f2f2;\n --color-surface: #eae9e9;\n --color-text: #201f1d;\n --color-accent: #b68235;\n --color-accent-2: #ac803e;\n --color-divider: color-mix(in srgb, #201f1d 16%, transparent);\n\n /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the\n same step of any role matches the others in visual value. */\n --color-neutral-100: #f8f4f4;\n --color-neutral-200: #eae7e7;\n --color-neutral-300: #d7d3d3;\n --color-neutral-400: #bab6b6;\n --color-neutral-500: #9b9797;\n --color-neutral-600: #7d7979;\n --color-neutral-700: #605d5d;\n --color-neutral-800: #444141;\n --color-neutral-900: #2d2b2b;\n\n --color-accent-100: #fff3e4;\n --color-accent-200: #ffe3bf;\n --color-accent-300: #facb8d;\n --color-accent-400: #e1ad66;\n --color-accent-500: #c28d41;\n --color-accent-600: #a06f24;\n --color-accent-700: #7d5411;\n --color-accent-800: #5a3b0a;\n --color-accent-900: #3a270d;\n\n --color-accent-2-100: #fff3e4;\n --color-accent-2-200: #ffe3be;\n --color-accent-2-300: #f5cd96;\n --color-accent-2-400: #dbaf70;\n --color-accent-2-500: #bc8f4e;\n --color-accent-2-600: #9b7232;\n --color-accent-2-700: #79561f;\n --color-accent-2-800: #573d14;\n --color-accent-2-900: #382810;\n\n --font-heading: \"Cormorant Garamond\", system-ui, sans-serif;\n --font-heading-weight: 600; /* review round 3: bold retired — semibold is the\n interface headings' ceiling (they need the weight at small sizes); display\n text goes lighter still, to the normal cut (see the deck) */\n --font-body: \"Lora\", system-ui, sans-serif;\n\n --space-1: 4.6px;\n --space-2: 9.2px;\n --space-3: 13.8px;\n --space-4: 18.4px;\n --space-6: 27.6px;\n --space-8: 36.8px;\n\n --radius-sm: 2px;\n --radius-md: 4px;\n --radius-lg: 7px;\n\n /* Elevation — derived from the ground: soft ink-tinted shadows on a\n light theme, a hairline edge + ambient darkness on a dark one. */\n --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);\n --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);\n --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);\n}\n\nbody {\n background: var(--color-bg);\n color: var(--color-text);\n font-family: var(--font-body);\n}\nh1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }\n\n.plate{filter:sepia(0.22) saturate(0.82) contrast(1.05);box-sizing:border-box;\n border:6px solid var(--color-surface);outline:1px solid var(--color-divider)}\n\n/* ══════════════════════════════════════════════════════════════════════════\n Components — built with the tokens above. Plain CSS\n on plain HTML: no JavaScript, no build step. Each class is documented in\n readme.md and demonstrated in foundations/ and components/.\n ══════════════════════════════════════════════════════════════════════ */\n\n*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);\n}\nh1 { font-size: 42px; }\nh2 { font-size: 32px; }\nh3 { font-size: 25px; }\nh4 { font-size: 20px; }\nh5 { font-size: 16px; }\nh6 { font-size: 13px; }\nh6 { letter-spacing: 0.08em; text-transform: uppercase; }\np { margin: 0 0 var(--space-3); }\na { color: var(--color-accent); text-underline-offset: 3px; }\nimg { display: block; max-width: 100%; }\nfigure { margin: 0; }\nfigcaption {\n font-size: 11px; margin-top: var(--space-1);\n color: color-mix(in srgb, var(--color-text) 55%, transparent);\n}\n.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }\n\n/* — rules — */\n.hr {\n height: 1px; border: 0; margin: var(--space-4) 0;\n background: var(--color-divider);\n}\n\n/* — buttons — */\n.btn {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer; text-decoration: none;\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —\n the pair sits side by side in sign-up rows */\n background: transparent; border: 1px solid transparent;\n padding: var(--space-2) calc(var(--space-3) * 1.2);\n border-radius: var(--radius-md);\n}\n.btn svg { display: block; }\n.btn:disabled { opacity: 0.45; cursor: not-allowed; }\n.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }\n.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }\n.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }\n.btn-secondary { border-color: var(--color-divider); }\n.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }\n.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }\n.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }\n.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }\n.btn-icon { width: 36px; height: 36px; padding: 0; }\n.btn-block { width: 100%; margin-top: var(--space-2); }\n\n/* — forms — */\n.field > label {\n display: block; font-size: 12px; margin-bottom: 5px;\n color: color-mix(in srgb, var(--color-text) 70%, transparent);\n}\n.input {\n width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;\n font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);\n background: transparent;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }\n.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }\ntextarea.input { min-height: 90px; resize: vertical; }\n.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }\n.radio input, .seg-opt input {\n position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;\n}\n.radio .dot {\n width: 16px; height: 16px; flex: none; border-radius: 50%;\n border: 1.5px solid var(--color-divider);\n}\n.radio:hover .dot { border-color: var(--color-accent); }\n.radio input:checked + .dot {\n border-color: var(--color-accent); background: var(--color-accent);\n box-shadow: inset 0 0 0 4px var(--color-bg);\n}\n.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.seg {\n display: inline-flex; overflow: hidden;\n border: 1px solid var(--color-divider); border-radius: var(--radius-md);\n}\n.seg-opt {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 7px 12px; font-size: 13px; cursor: pointer;\n}\n.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }\n.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }\n.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }\n.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n\n/* — cards — */\n.card {\n display: flex; flex-direction: column; gap: var(--space-2);\n padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);\n}\n.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }\n.card-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 17px; line-height: 1.2;\n}\n.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }\n.card-meta {\n display: flex; align-items: center; gap: 6px; font-size: 11px;\n color: color-mix(in srgb, var(--color-text) 50%, transparent);\n}\n.elev-sm { box-shadow: var(--shadow-sm); }\n.elev-md { box-shadow: var(--shadow-md); }\n.elev-lg { box-shadow: var(--shadow-lg); }\n\n/* — tags — */\n.tag {\n display: inline-flex; align-items: center; font-size: 11px;\n letter-spacing: 0.02em; padding: 3px 10px;\n border-radius: calc(var(--radius-md) * 0.75);\n}\n.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }\n.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }\n.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }\n.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }\n\n/* — navigation — */\n.nav {\n display: flex; align-items: center; gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--color-divider);\n}\n.nav-brand {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 18px; margin-right: auto;\n}\n.nav a { color: inherit; text-decoration: none; font-size: 14px; }\n.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }\n\n/* — tables — */\n.table { width: 100%; border-collapse: collapse; font-size: 14px; }\n.table th {\n text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;\n color: color-mix(in srgb, var(--color-text) 60%, transparent);\n padding: var(--space-2); border-bottom: 1px solid var(--color-divider);\n}\n.table td {\n padding: var(--space-2);\n border-bottom: 1px solid var(--color-divider);\n}\n.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }\n\n/* — dialog — */\n.dialog-backdrop {\n position: fixed; inset: 0; display: grid; place-items: center;\n padding: var(--space-4);\n background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);\n}\n.dialog {\n width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);\n padding: var(--space-4); border-radius: var(--radius-lg);\n background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);\n}\n.dialog-title {\n font-family: var(--font-heading); font-weight: var(--font-heading-weight);\n font-size: 20px;\n}\n.dialog-body { font-size: 14px; opacity: 0.85; }\n.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }\n\n<\u002Fstyle>\n <style>/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"6c1930a1-55c7-45db-95c3-623f1459f9be\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"5bd07e81-1765-4b18-808c-c34c56467c93\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"397cab2d-66ac-4207-a7dc-c1a1b688a5b9\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"92995646-7006-40e7-8fbf-11c17bb73cf1\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"3fc6f1f0-669d-440f-8c89-d7c4cc55921d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"534cf078-ef38-480f-851e-74b9e7073514\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"fefee7c2-5020-47d6-99cb-43c48ae4a77d\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"6c1930a1-55c7-45db-95c3-623f1459f9be\") format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"5bd07e81-1765-4b18-808c-c34c56467c93\") format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"397cab2d-66ac-4207-a7dc-c1a1b688a5b9\") format('woff2');\n unicode-range: U+1F00-1FFF;\n}\n/* greek */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"92995646-7006-40e7-8fbf-11c17bb73cf1\") format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"3fc6f1f0-669d-440f-8c89-d7c4cc55921d\") format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"534cf078-ef38-480f-851e-74b9e7073514\") format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Alegreya';\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(\"fefee7c2-5020-47d6-99cb-43c48ae4a77d\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: italic;\n font-weight: 400;\n font-display: swap;\n src: url(\"baf7348b-eb32-43ea-a01f-e67d852a2856\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"4b162550-3730-49f5-94e0-5e5d1133e3b6\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* latin */\n@font-face {\n font-family: 'Comic Neue';\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(\"6c43c3db-41c1-466a-ab0a-0589aceca239\") format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n<\u002Fstyle>\n <script src=\"016285e6-4d1c-400d-ba40-6a39311de7f0\"><\u002Fscript>\n <style>\n :root { --font-heading: 'Alegreya', 'Cormorant Garamond', serif; --font-body: 'Comic Neue', 'Edu SA Beginner', sans-serif; }\n html { scroll-snap-type: y proximity; }\n body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }\n a { color: var(--color-accent-700); text-decoration: none; }\n a:hover { color: var(--color-accent-600); }\n @media (max-width: 1500px) { img[data-deco] { display: none; } }\n button:focus-visible, textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n <\u002Fstyle>\n<\u002Fhelmet>\n<div style=\"min-height: 100vh;\">\n <header style=\"position: sticky; top: 0; z-index: 20; border-bottom: none; background: var(--color-bg) linear-gradient(90deg, #33517a 0 33%, #b5372f 33% 66%, #c9973f 66% 100%) bottom / 100% 3px no-repeat; padding: var(--space-3) var(--space-8); display: flex; align-items: center; gap: var(--space-6);\">\n <span style=\"width: 110px; flex: none;\"><\u002Fspan>\n <div style=\"flex: 1; text-align: center;\">\n <span style=\"font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-neutral-600);\">Free Writing 4 · pairs with Worksheet 11 — <\u002Fspan>\n <span style=\"font-family: var(--font-heading); font-size: 24px; font-weight: 600;\">My Cold Door<\u002Fspan>\n <\u002Fdiv>\n <div style=\"display: flex; align-items: center; gap: var(--space-4);\">\n <img src=\"850f90bc-7b0d-4fd3-bc3f-7565ec59809a\" alt=\"\" style=\"height: 28px;\"><div style=\"font-variant-numeric: tabular-nums; font-size: 22px;\"><span style=\"color: var(--color-accent-700); font-weight: 600;\">{{ earned }}<\u002Fspan><span style=\"color: var(--color-neutral-600);\"> / 10<\u002Fspan><\u002Fdiv>\n <button sc-camel-on-click=\"{{ toggleTeacher }}\" title=\"Teacher notes\" style=\"background: {{ teacherBg }}; border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: 6px 12px; font-family: var(--font-body); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); cursor: pointer;\">Teacher<\u002Fbutton>\n <\u002Fdiv>\n <\u002Fheader>\n <main style=\"max-width: 1320px; margin: 0 auto; padding: var(--space-4) var(--space-8) calc(var(--space-8) * 2); box-sizing: border-box;\">\n <p style=\"font-size: 24px; margin: 0 0 var(--space-4);\"><strong>Pacy’s “cold door” is becoming an author and illustrator. What is your cold door<\u002Fstrong> — something that looks hard, but you want to try it anyway? 6–8 sentences; count your sentences, not your words.<\u002Fp>\n <sc-if value=\"{{ teacher }}\" hint-placeholder-val=\"{{ false }}\">\n <div style=\"border: 1px solid var(--color-accent-300); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); background: var(--color-accent-100); padding: var(--space-4) var(--space-4); margin-bottom: var(--space-6); font-size: 18px; line-height: 1.65;\">\n <div style=\"font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: var(--space-2);\">Teacher<\u002Fdiv>\n <p style=\"margin: 0;\">Pairs with Worksheet 11 · writing complete sentences. Rubric: Ideas 4 · Sentences 4 (no fragments or run-ons) · Spelling & punctuation 2 — then rewrite the corrected version in the journal.<\u002Fp>\n <\u002Fdiv>\n <\u002Fsc-if>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #e2e9f3; border: 1.5px solid #33517a; color: #263d5c; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">1<\u002Fspan>Read the example · spot the toolbox<\u002Fh2>\n <\u002Fdiv>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); align-items: start;\">\n <div style=\"border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-4); font-size: 24px; line-height: 1.7;\">\n My cold door is speaking English in front of the whole class. My voice goes quiet, and my hands get cold. Last month my teacher asked me to read a poem aloud. I wanted to say no. Then I remembered that Pacy jumped through her cold door anyway. I read the poem, and only one word came out wrong. The door was not as cold as I thought.\n <\u002Fdiv>\n <div style=\"border: 1px solid var(--color-divider); border-left: 4px solid #33517a; border-radius: var(--radius-md); padding: var(--space-4); font-size: 20px; line-height: 1.7;\">\n <div style=\"font-size: 15px; letter-spacing: 0.12em; text-transform: uppercase; color: #263d5c; margin-bottom: var(--space-2);\">Sentence toolbox · complete sentences<\u002Fdiv>\n A complete sentence has a <strong>subject<\u002Fstrong>, a <strong>predicate<\u002Fstrong>, and one whole thought.<br>\n Fragment: <em>Wanted to say no.<\u002Fem> → <em>I wanted to say no.<\u002Fem><br>\n Run-on: <em>I read the poem only one word came out wrong.<\u002Fem> → split it with a period, or join it with <strong>and<\u002Fstrong>.\n <\u002Fdiv>\n <\u002Fdiv>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; margin-bottom: var(--space-8); display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f7e3e1; border: 1.5px solid #b5372f; color: #8e2a24; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">2<\u002Fspan>Plan — words and phrases, not sentences yet<\u002Fh2>\n <\u002Fdiv>\n <ul style=\"font-size: 22px; line-height: 1.8; margin: 0 0 var(--space-4); padding-left: var(--space-6); color: var(--color-neutral-700);\">\n <li>What is hard for you — a subject, a sport, speaking, meeting new people?<\u002Fli>\n <li>What happens in your body when you have to do it?<\u002Fli>\n <li>Has anyone ever helped you, or pushed you to try?<\u002Fli>\n <li>What would happen if you jumped through the door anyway?<\u002Fli>\n <\u002Ful>\n <textarea rows=\"4\" placeholder=\"Jot the class plan here…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 22px; line-height: 1.6; padding: var(--space-3); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical;\"><\u002Ftextarea>\n <\u002Fsection>\n\n <section style=\"min-height: calc(100vh - 130px); box-sizing: border-box; scroll-snap-align: start; scroll-margin-top: 76px; display: flex; flex-direction: column;\">\n <div style=\"display: flex; align-items: baseline; gap: var(--space-4); border-bottom: 1px solid var(--color-divider); padding-bottom: var(--space-3); margin-bottom: var(--space-4);\">\n <h2 style=\"font-family: var(--font-heading); font-weight: 600; font-size: 34px; margin: 0;\"><span style=\"display: inline-flex; width: 42px; height: 42px; border-radius: 50%; background: #f4e7d3; border: 1.5px solid #c9973f; color: #7c5520; align-items: center; justify-content: center; font-size: 28px; vertical-align: middle; margin-right: 12px; flex: none;\">3<\u002Fspan>Write it — then mark it<\u002Fh2>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <div style=\"font-variant-numeric: tabular-nums; font-size: 17px; color: var(--color-neutral-600);\">6–8 sentences · about 65–90 words<\u002Fdiv>\n <\u002Fdiv>\n <p style=\"font-style: italic; color: #8e2a24; font-size: 19px; margin: 0 0 var(--space-3);\">Challenge: read your paragraph aloud slowly. Every time you stop for a breath, check that there is a full stop there.<\u002Fp>\n <textarea rows=\"7\" placeholder=\"My title… then the paragraph, written together…\" style=\"width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 24px; line-height: 1.7; padding: var(--space-4); border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent; color: var(--color-text); resize: vertical; margin-bottom: var(--space-4);\"><\u002Ftextarea>\n <div style=\"display: flex; gap: var(--space-6); flex-wrap: wrap; border-top: 1px solid var(--color-divider); padding-top: var(--space-4); align-items: center;\">\n <sc-for list=\"{{ rubric }}\" as=\"r\" hint-placeholder-count=\"3\">\n <div style=\"display: flex; gap: var(--space-2); align-items: center;\">\n <span style=\"font-size: 19px; color: var(--color-neutral-700);\">{{ r.label }}<\u002Fspan>\n <sc-for list=\"{{ r.pts }}\" as=\"p\" hint-placeholder-count=\"4\">\n <button sc-camel-on-click=\"{{ p.pick }}\" style=\"width: 44px; height: 44px; border-radius: 50%; font-family: var(--font-body); font-size: 20px; font-variant-numeric: tabular-nums; border: 1.5px solid {{ p.border }}; background: {{ p.bg }}; color: {{ p.color }}; cursor: pointer;\">{{ p.n }}<\u002Fbutton>\n <\u002Fsc-for>\n <\u002Fdiv>\n <\u002Fsc-for>\n <div style=\"flex: 1;\"><\u002Fdiv>\n <span style=\"font-size: 19px; font-style: italic; color: var(--color-neutral-600);\">Then rewrite the corrected version neatly in your journal.<\u002Fspan>\n <\u002Fdiv>\n <\u002Fsection>\n <img data-deco=\"\" src=\"16f1198d-663f-4528-9efb-063a5d57f9ba\" alt=\"\" style=\"position: fixed; right: 18px; bottom: 12px; height: 170px; z-index: 5; pointer-events: none; opacity: 0.92;\">\n <\u002Fmain>\n<\u002Fdiv>\n<\u002Fx-dc>\n<script type=\"text/x-dc\" data-dc-script=\"\">\nclass Component extends DCLogic {\n state = { awarded: { 0: 0, 1: 0, 2: 0 }, teacher: false };\n RUBRIC = [['Ideas', 4], ['Sentences', 4], ['Spelling & punctuation', 2]];\n renderVals() {\n const s = this.state;\n const rubric = this.RUBRIC.map(([label, max], ri) => {\n const cur = s.awarded[ri] || 0;\n return { label, pts: Array.from({ length: max }, (_, k) => k + 1).map(n => ({ n,\n bg: n <= cur ? 'var(--color-accent-100)' : 'transparent',\n border: n <= cur ? 'var(--color-accent)' : 'var(--color-divider)',\n color: n <= cur ? 'var(--color-accent-700)' : 'var(--color-neutral-600)',\n pick: () => this.setState(p => ({ awarded: { ...p.awarded, [ri]: (p.awarded[ri] === n ? 0 : n) } })) })) };\n });\n return { earned: (s.awarded[0] || 0) + (s.awarded[1] || 0) + (s.awarded[2] || 0),\n teacher: s.teacher, rubric,\n teacherBg: s.teacher ? 'var(--color-accent-100)' : 'transparent',\n toggleTeacher: () => this.setState(p => ({ teacher: !p.teacher })) };\n }\n}\n<\u002Fscript>\n\n\n<\u002Fbody><\u002Fhtml>"
</script>
</body>
</html>