{"roots":["7:126"],"nodeById":{"7:126":{"type":"WEBPAGE","id":"7:126","name":"/flashcards","absoluteBoundingBox":{"x":3774.0,"y":0.0,"width":1847.0,"height":1196.0},"isolatedAbsoluteRenderBounds":{"x":3774.0,"y":0.0,"width":1847.0,"height":1196.0},"relativeTransform":[[1.0,0.0,3774.0],[0.0,1.0,0.0]],"size":{"x":1847.0,"y":1196.0},"fills":[{"opacity":0.0470588244497776,"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"visible":true}],"strokeAlign":"INSIDE","strokes":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"children":["7:127","7:140"]},"7:149":{"type":"WIDGET","id":"7:149","name":"Embed 1","absoluteBoundingBox":{"x":5202.0,"y":269.0,"width":335.0,"height":843.0},"isolatedAbsoluteRenderBounds":{"x":5202.0,"y":269.0,"width":335.0,"height":843.0},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,169.0]],"size":{"x":335.0,"y":843.0},"layoutAlign":"STRETCH","accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"syncedState":{"embedAllowFullscreen":"false","embedCodeType":"html","embedIframeHtml":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Flashcards</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n<style>\n  :root {\n    --bg:        #202020;\n    --surface:   #2C2C2C;\n    --text:      #F8F8F8;\n    --text-dim:  #A0A0A0;\n    --text-mute: #6E6E6E;\n    --line:      #3A3A3A;\n\n    --yellow:    #EBF400;\n    --green:     #00D45D;\n    --orange:    #FF7A18;\n    --blue:      #6A89FF;\n    --pink:      #FF90CC;\n    --red:       #FF4C56;\n    --sky:       #8FD0FF;\n\n    --ink:       #151515;   /* text that sits on a colour fill */\n    --accent:    var(--orange);\n    --radius:    999px;\n  }\n\n  * { box-sizing: border-box; }\n  body {\n    margin: 0; padding: 16px;\n    font-family: \"IBM Plex Sans\", ui-sans-serif, -apple-system, sans-serif;\n    color: var(--text); background: var(--bg);\n    -webkit-font-smoothing: antialiased;\n  }\n  .wrap { max-width: 820px; margin: 0 auto; }\n\n  .bar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; }\n  .bar.controls { margin-bottom: 12px; }\n  .spacer { flex: 1 1 auto; }\n\n  /* Pills */\n  button, select {\n    font: inherit; font-size: 14px; font-weight: 600;\n    padding: 11px 22px; border-radius: var(--radius);\n    border: none; background: var(--surface); color: var(--text);\n    cursor: pointer; transition: transform .1s ease, filter .1s ease;\n    -webkit-appearance: none; appearance: none;\n  }\n  select {\n    padding-right: 38px;\n    background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23F8F8F8' stroke-width='1.8' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\");\n    background-repeat: no-repeat; background-position: right 16px center;\n  }\n  button:hover, select:hover { filter: brightness(1.25); }\n  button:active { transform: scale(.97); }\n  button.primary { background: var(--accent); color: var(--ink); }\n  button.primary:hover { filter: brightness(1.08); }\n\n  /* Card — the hero pill */\n  .card {\n    background: var(--accent); color: var(--ink);\n    border-radius: 40px; padding: 48px 36px;\n    min-height: 320px;                  /* both faces share one height — no jump on flip */\n    display: flex; flex-direction: column; align-items: center; justify-content: center;\n    text-align: center; cursor: pointer; user-select: none;\n    transition: filter .12s ease;\n  }\n  .card:hover { filter: brightness(1.06); }\n  .card-stage { perspective: 1400px; min-height: 320px; }\n  body { overflow-x: hidden; }\n  .card { transform-style: preserve-3d; will-change: transform; }\n  @media (prefers-reduced-motion: reduce) {\n    .card { transition: filter .12s ease !important; }\n  }\n  .card.flip { background: var(--green); }\n\n  .hint {\n    font-family: \"IBM Plex Mono\", ui-monospace, monospace;\n    font-size: 11px; text-transform: uppercase; letter-spacing: .12em;\n    color: var(--ink); opacity: .55; margin-bottom: 20px; font-weight: 500;\n  }\n  .q { font-size: 26px; font-weight: 700; line-height: 1.25; letter-spacing: -0.02em; max-width: 640px; }\n  .a { font-size: 20px; font-weight: 500; line-height: 1.45; max-width: 640px; }\n\n  .meta { display: flex; gap: 8px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }\n  .tag {\n    font-family: \"IBM Plex Mono\", monospace;\n    font-size: 11px; font-weight: 500; padding: 5px 12px;\n    border-radius: var(--radius); background: rgba(21,21,21,.14); color: var(--ink);\n  }\n\n  .nav { display: flex; gap: 12px; align-items: center; justify-content: center; margin-top: 18px; }\n  .prog {\n    font-family: \"IBM Plex Mono\", monospace; font-size: 12px;\n    color: var(--text-mute); min-width: 74px; text-align: center;\n  }\n  .stats {\n    display: flex; gap: 16px; justify-content: center; margin-top: 14px;\n    font-family: \"IBM Plex Mono\", monospace; font-size: 12px; color: var(--text-mute);\n  }\n  .known { color: var(--green); } .again { color: var(--orange); }\n\n  .state { text-align: center; color: var(--text-mute); padding: 56px 20px; font-size: 15px; line-height: 1.6; }\n  .err { color: var(--red); font-size: 14px; line-height: 1.5; }\n\n  .setup { background: var(--surface); border-radius: 32px; padding: 36px 32px; }\n  .setup .lead { margin: 0 0 16px; font-size: 20px; font-weight: 700; letter-spacing: -0.02em; }\n  .setup input {\n    width: 100%; font: inherit; font-size: 14px; padding: 13px 20px;\n    border: none; border-radius: var(--radius);\n    background: var(--bg); color: var(--text);\n  }\n  .setup textarea {\n    width: 100%; font-family: \"IBM Plex Mono\", monospace; font-size: 13px; line-height: 1.6;\n    padding: 16px 20px; border: none; border-radius: 20px;\n    background: var(--bg); color: var(--text); resize: vertical;\n  }\n  .setup textarea:focus { outline: 2px solid var(--accent); }\n  .setup input::placeholder, .setup textarea::placeholder { color: var(--text-mute); }\n  .setup input:focus { outline: 2px solid var(--accent); outline-offset: 0; }\n  .help { font-size: 13px; line-height: 1.65; color: var(--text-mute); margin: 18px 0 0; }\n  .note {\n    margin-top: 22px; padding: 18px 20px; border-radius: 20px;\n    background: var(--bg); border: 1px dashed var(--line);\n  }\n  .note-head {\n    display: flex; gap: 14px; align-items: center; justify-content: space-between; flex-wrap: wrap;\n    font-size: 14px; font-weight: 600; color: var(--text);\n  }\n  .note-head button { font-size: 13px; padding: 8px 16px; background: var(--accent); color: var(--ink); }\n  .note .help { margin: 0; }\n  .help b { color: var(--text-dim); font-weight: 600; }\n\n  .list {\n    display: flex; flex-direction: column; gap: 8px;\n    max-height: 70vh; overflow-y: auto;\n    padding: 0 6px 8px 0;\n    overscroll-behavior: contain;\n  }\n  .list::-webkit-scrollbar { width: 8px; }\n  .list::-webkit-scrollbar-track { background: transparent; }\n  .list::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }\n  .list::-webkit-scrollbar-thumb:hover { background: var(--text-mute); }\n  .row {\n    display: flex; gap: 14px; align-items: flex-start;\n    background: var(--surface); border-radius: 20px; padding: 16px 22px; font-size: 14px;\n  }\n  .rq { flex: 1; font-weight: 600; color: var(--text); }\n  .ra { flex: 1; color: var(--text-dim); font-weight: 400; }\n  /* On a dark row the default ink-on-translucent tag is illegible — give it a real fill. */\n  .row .tag { background: transparent; color: var(--text-dim); border: 1px solid var(--line); flex: 0 0 auto; }\n</style>\n</head>\n<body>\n<div class=\"wrap\" id=\"app\"><div class=\"state\">Loading cards…</div></div>\n\n<script>\nconst STORE_KEY  = \"flashcards.progress.v1\";  // review stats, per device\nconst CACHE_KEY  = \"flashcards.cache.v1\";     // the deck itself, per device\n\nconst $ = (h) => { document.getElementById(\"app\").innerHTML = h; };\n\n/* If anything throws at parse/run time, say so instead of hanging on \"Loading…\" */\nwindow.onerror = function (msg, src, line) {\n  try {\n    document.getElementById(\"app\").innerHTML =\n      '<div class=\"state err\">Script error: ' + msg + ' (line ' + line + ')</div>';\n  } catch (e) {}\n};\nconst esc = (s) => String(s).replace(/[&<>\"]/g, c => ({ \"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\" }[c]));\n\n/* --- tiny CSV parser: handles quoted fields, commas, newlines --- */\nfunction parseCSV(text) {\n  const rows = []; let row = [], field = \"\", q = false;\n  for (let i = 0; i < text.length; i++) {\n    const c = text[i];\n    if (q) {\n      if (c === '\"') { if (text[i+1] === '\"') { field += '\"'; i++; } else q = false; }\n      else field += c;\n    } else if (c === '\"') q = true;\n    else if (c === \",\") { row.push(field); field = \"\"; }\n    else if (c === \"\\n\") { row.push(field); rows.push(row); row = []; field = \"\"; }\n    else if (c !== \"\\r\") field += c;\n  }\n  if (field.length || row.length) { row.push(field); rows.push(row); }\n  return rows.filter(r => r.some(v => v.trim() !== \"\"));\n}\n\n\n/* Parse pasted text into cards. Accepts, in order of preference:\n     1. CSV / TSV with a question,answer,category header (paste straight from a sheet)\n     2. Q: ... / A: ... / Cat: ... blocks\n     3. One card per line:  question | answer | category\n   Blank lines separate cards in format 2. */\nfunction parsePasted(text) {\n  const t = (text || \"\").trim();\n  if (!t) return [];\n\n  // 1. Sheet paste — tab or comma separated, with a header row\n  const firstLine = t.split(\"\\n\")[0].toLowerCase();\n  if (firstLine.includes(\"question\") && firstLine.includes(\"answer\")) {\n    const isTab = firstLine.includes(\"\\t\");\n    const rows = isTab\n      ? t.split(\"\\n\").map(r => r.split(\"\\t\"))\n      : parseCSV(t);\n    try { return rowsToCards(rows); } catch (e) { /* fall through */ }\n  }\n\n  // 3. Pipe-delimited, one per line\n  if (t.split(\"\\n\").every(l => !l.trim() || l.includes(\"|\"))) {\n    const out = t.split(\"\\n\").filter(l => l.trim()).map((l, n) => {\n      const p = l.split(\"|\").map(s => s.trim());\n      return { id: \"p\" + n, q: p[0] || \"\", a: p[1] || \"\", cat: p[2] || \"Uncategorized\" };\n    }).filter(c => c.q && c.a);\n    if (out.length) return out;\n  }\n\n  // 2. Q:/A: blocks\n  const out = [];\n  let cur = null, n = 0;\n  for (const raw of t.split(\"\\n\")) {\n    const line = raw.trim();\n    if (!line) { continue; }\n    const mq = line.match(/^(?:\\d+[.)]\\s*)?(?:q|question)\\s*[:.\\-]\\s*(.+)$/i);\n    const ma = line.match(/^(?:a|answer)\\s*[:.\\-]\\s*(.+)$/i);\n    const mc = line.match(/^(?:cat|category)\\s*[:.\\-]\\s*(.+)$/i);\n    if (mq) {\n      if (cur && cur.q && cur.a) out.push(cur);\n      cur = { id: \"p\" + (n++), q: mq[1].trim(), a: \"\", cat: \"Uncategorized\" };\n    } else if (ma && cur) {\n      cur.a = ma[1].trim();\n    } else if (mc && cur) {\n      cur.cat = mc[1].trim();\n    } else if (cur && cur.a) {\n      cur.a += \" \" + line;          // wrapped answer line\n    }\n  }\n  if (cur && cur.q && cur.a) out.push(cur);\n  return out;\n}\n\nfunction rowsToCards(rows) {\n  const head = rows[0].map(h => h.trim().toLowerCase());\n  const iq = head.indexOf(\"question\");\n  const ia = head.indexOf(\"answer\");\n  const ic = head.indexOf(\"category\");\n  if (iq === -1 || ia === -1) throw new Error('Sheet needs \"question\" and \"answer\" columns.');\n  return rows.slice(1).map((r, n) => ({\n    id: \"c\" + n,\n    q: (r[iq] || \"\").trim(),\n    a: (r[ia] || \"\").trim(),\n    cat: (ic > -1 && r[ic] ? r[ic].trim() : \"Uncategorized\"),\n  })).filter(c => c.q);\n}\n\n/* --- progress: localStorage, works fine on a real website --- */\nfunction loadProgress() {\n  try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {}; } catch { return {}; }\n}\nfunction saveProgress(p) {\n  try { localStorage.setItem(STORE_KEY, JSON.stringify(p)); } catch {}\n}\n\nlet CARDS = [], PROGRESS = loadProgress();\n\n/* Colour flicks through the palette as you move card to card. */\nconst PALETTE = [\"#FF7A18\", \"#00D45D\", \"#FF90CC\", \"#6A89FF\", \"#EBF400\", \"#8FD0FF\", \"#FF4C56\"];\nconst cardColor = (n) => PALETTE[((n % PALETTE.length) + PALETTE.length) % PALETTE.length];\nlet view = \"review\", cat = \"All\", order = \"newest\", idx = 0, flipped = false;\n\nfunction deck() {\n  let d = CARDS.filter(c => cat === \"All\" || c.cat === cat);\n  if (order === \"oldest\") d = [...d].reverse();\n  if (order === \"shuffle\") d = [...d].sort(() => Math.random() - 0.5);\n  if (order === \"weakest\") d = [...d].sort((a, b) => score(b) - score(a));\n  return d;\n}\nconst stat = (c) => PROGRESS[c.q] || { known: 0, again: 0 };\nconst score = (c) => stat(c).again - stat(c).known;\n\nfunction mark(field) {\n  const c = deck()[idx]; if (!c) return;\n  const s = stat(c);\n  PROGRESS[c.q] = { known: s.known + (field === \"known\" ? 1 : 0), again: s.again + (field === \"again\" ? 1 : 0) };\n  saveProgress(PROGRESS);\n  flipped = false; idx = (idx + 1) % Math.max(deck().length, 1);\n  render();\n}\n\nlet FLIPPING = false;\nfunction doFlip() {\n  if (FLIPPING) return;\n  const el = document.querySelector(\".card\");\n  if (!el) { flipped = !flipped; render(); return; }\n  if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n    flipped = !flipped; render(); return;\n  }\n  FLIPPING = true;\n  el.style.transition = \"transform .16s ease-in\";\n  el.style.transform  = \"rotateY(90deg)\";\n  setTimeout(() => {\n    flipped = !flipped;\n    render();                                   // repaint the new face while side-on\n    const next = document.querySelector(\".card\");\n    if (next) {\n      next.style.transition = \"none\";\n      next.style.transform  = \"rotateY(-90deg)\";\n      requestAnimationFrame(() => {\n        next.style.transition = \"transform .18s ease-out\";\n        next.style.transform  = \"rotateY(0deg)\";\n      });\n    }\n    FLIPPING = false;\n  }, 160);\n}\n\nfunction render() {\n  const cats = [\"All\", ...[...new Set(CARDS.map(c => c.cat))].sort()];\n  const d = deck();\n  const c = d[idx];\n  const s = c ? stat(c) : { known: 0, again: 0 };\n\n  let body;\n  if (!c) {\n    body = `<div class=\"state\">No cards in this category.</div>`;\n  } else if (view === \"review\") {\n    body = `\n      <div class=\"card-stage\"><div class=\"card\" style=\"background:${flipped ? \"var(--surface)\" : cardColor(idx)};color:${flipped ? \"var(--text)\" : \"var(--ink)\"}\" onclick=\"doFlip()\">\n        <div class=\"hint\" style=\"color:${flipped ? \"var(--text-mute)\" : \"var(--ink)\"};opacity:${flipped ? 1 : .55}\">${flipped ? \"Answer\" : \"Question — tap to flip\"}</div>\n        <div class=\"${flipped ? \"a\" : \"q\"}\">${esc(flipped ? c.a : c.q)}</div>\n        <div class=\"meta\">\n          <span class=\"tag\" style=\"background:${flipped ? cardColor(idx) : \"rgba(21,21,21,.14)\"};color:var(--ink)\">${esc(c.cat)}</span>\n        </div>\n      </div></div>\n      <div class=\"nav\">\n        <button onclick=\"flipped=false;idx=(idx-1+${d.length})%${d.length};render()\">←</button>\n        <span class=\"prog\">${idx + 1} / ${d.length}</span>\n        <button onclick=\"flipped=false;idx=(idx+1)%${d.length};render()\">→</button>\n      </div>\n      <div class=\"nav\" style=\"margin-top:10px\">\n        <button onclick=\"mark('again')\">Again</button>\n        <button class=\"primary\" onclick=\"mark('known')\">Got it</button>\n      </div>\n      <div class=\"stats\">\n        <span class=\"known\">Got it: ${s.known}</span>\n        <span class=\"again\">Again: ${s.again}</span>\n      </div>`;\n  } else {\n    body = `<div class=\"list\">${d.map((x, i) => `\n      <div class=\"row\">\n        <div class=\"rq\">${esc(x.q)}</div>\n        <div class=\"ra\">${esc(x.a)}</div>\n        <span class=\"tag\" style=\"border-color:${cardColor(i)};color:${cardColor(i)}\">${esc(x.cat)}</span>\n      </div>`).join(\"\")}</div>`;\n  }\n\n  $(`\n    <div class=\"bar controls\">\n      <button class=\"${view === \"review\" ? \"primary\" : \"\"}\" onclick=\"view='review';render()\">Review</button>\n      <button class=\"${view === \"browse\" ? \"primary\" : \"\"}\" onclick=\"view='browse';render()\">Browse (${CARDS.length})</button>\n    </div>\n    <div class=\"bar\">\n      <select onchange=\"cat=this.value;idx=0;flipped=false;render()\">\n        ${cats.map(x => `<option ${x === cat ? \"selected\" : \"\"}>${esc(x)}</option>`).join(\"\")}\n      </select>\n      <select onchange=\"order=this.value;idx=0;flipped=false;render()\">\n        <option value=\"newest\" ${order===\"newest\"?\"selected\":\"\"}>Newest first</option>\n        <option value=\"oldest\" ${order===\"oldest\"?\"selected\":\"\"}>Oldest first</option>\n        <option value=\"shuffle\" ${order===\"shuffle\"?\"selected\":\"\"}>Shuffle</option>\n        <option value=\"weakest\" ${order===\"weakest\"?\"selected\":\"\"}>Weakest first</option>\n      </select>\n      <span class=\"spacer\"></span>\n      <button onclick=\"setupScreen('')\">New cards</button>\n      <button onclick=\"if(confirm('Reset your review stats on this device?')){PROGRESS={};saveProgress({});render()}\">Reset</button>\n    </div>\n    ${body}\n  `);\n}\n\n\nconst LLM_PROMPT = `Turn the content below into flashcards.\n\nRules:\n- One fact per card. If a sentence holds two facts, split it into two cards.\n- The question must be a real question, never answerable with yes/no.\n- The answer must be the shortest complete answer — ideally under 20 words.\n- Skip filler, narrative and opinion. Only what is worth memorising.\n- Group the cards into a few sensible categories you choose from the content.\n\nReturn ONLY cards in exactly this format, nothing else:\n\nQ: <question>\nA: <answer>\nCat: <category>\n\nQ: <question>\nA: <answer>\nCat: <category>\n\nHere is the content:\n`;\n\nfunction setupScreen(errMsg) {\n  $(`\n    <div class=\"setup\">\n      <p class=\"lead\">Paste your cards.</p>\n      <textarea id=\"pasted\" rows=\"9\" placeholder=\"Q: What is a tripwire?\nA: A pre-agreed condition that reopens the plan.\nCat: Deciding\n\nQ: Who does a unanimity rule reward?\nA: Whoever digs in hardest.\nCat: Deciding\"></textarea>\n      <div class=\"bar\" style=\"margin-top:14px\">\n        <button class=\"primary\" onclick=\"applyPasted('add')\">Add to deck</button>\n        <button onclick=\"applyPasted('replace')\">Replace deck</button>\n        ${CARDS.length ? `<span class=\"prog\" style=\"min-width:auto\">${CARDS.length} cards in deck</span>` : \"\"}\n      </div>\n      ${errMsg ? `<p class=\"err\">${esc(errMsg)}</p>` : \"\"}\n\n      <div class=\"note\">\n        <div class=\"note-head\">\n          <span>Got raw material instead? Turn it into cards with any LLM.</span>\n          <button id=\"copybtn\" onclick=\"copyPrompt()\">Copy prompt</button>\n        </div>\n        <p class=\"help\" style=\"margin-top:10px\">\n          Copy the prompt, paste it into ChatGPT / Claude / anything else along with your\n          article, notes or transcript, then paste the result back in the box above.\n        </p>\n      </div>\n\n      <p class=\"help\">\n        Cards also accept <b>question | answer | category</b> per line, or rows pasted\n        straight out of a spreadsheet. Your deck is saved on this device.\n      </p>\n    </div>\n  `);\n}\n\nfunction copyPrompt() {\n  const btn = document.getElementById(\"copybtn\");\n  const done = () => { if (btn) { btn.textContent = \"Copied\"; setTimeout(() => { btn.textContent = \"Copy prompt\"; }, 1600); } };\n  if (navigator.clipboard && navigator.clipboard.writeText) {\n    navigator.clipboard.writeText(LLM_PROMPT).then(done, fallback);\n  } else fallback();\n  function fallback() {\n    const ta = document.createElement(\"textarea\");\n    ta.value = LLM_PROMPT;\n    ta.style.position = \"fixed\"; ta.style.opacity = \"0\";\n    document.body.appendChild(ta); ta.select();\n    try { document.execCommand(\"copy\"); done(); } catch (e) {}\n    document.body.removeChild(ta);\n  }\n}\n\nfunction applyPasted(mode) {\n  const el = document.getElementById(\"pasted\");\n  const incoming = parsePasted(el ? el.value : \"\");\n  if (!incoming.length) {\n    setupScreen(\"Couldn't find any cards in that. Check the format below.\");\n    return;\n  }\n\n  if (mode === \"replace\") {\n    CARDS = incoming;\n    // Stats are keyed on the question, so old ones would linger. Drop them.\n    const keep = new Set(incoming.map(c => c.q));\n    PROGRESS = Object.fromEntries(Object.entries(PROGRESS).filter(([q]) => keep.has(q)));\n    saveProgress(PROGRESS);\n  } else {\n    // Add on top, skipping any question that's already in the deck.\n    const seen = new Set(CARDS.map(c => c.q.trim().toLowerCase()));\n    const fresh = incoming.filter(c => !seen.has(c.q.trim().toLowerCase()));\n    CARDS = [...CARDS, ...fresh];\n  }\n  CARDS = CARDS.map((c, n) => ({ ...c, id: \"c\" + n }));   // keep ids unique after a merge\n\n  try { localStorage.setItem(CACHE_KEY, JSON.stringify(CARDS)); } catch (e) {}\n  idx = 0; flipped = false; cat = \"All\";\n  render();\n}\n\n\n\nfunction boot() {\n  let saved = null;\n  try { saved = JSON.parse(localStorage.getItem(CACHE_KEY)); } catch (e) {}\n  if (saved && saved.length) { CARDS = saved; render(); }\n  else setupScreen(\"\");\n}\n\nboot();\n</script>\n</body>\n</html>","embedURL":""}},"7:143":{"type":"TEXT","id":"7:143","name":"Flashcards","absoluteBoundingBox":{"x":5285.0,"y":130.0,"width":122.0,"height":31.0},"isolatedAbsoluteRenderBounds":{"x":5286.9677734375,"y":137.240005493164,"width":118.876953125,"height":18.0479888916016},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,10.0]],"size":{"x":122.0,"y":31.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.123685397207737,"g":0.123685397207737,"b":0.123685397207737,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Flashcards","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"resolvedParagraphValues":{"0":{"listSpacing":0.0,"paragraphIndent":0.0,"paragraphSpacing":0.0}},"textAutoResize":"WIDTH_AND_HEIGHT","textAlignVertical":"CENTER","style":{"fontFamily":"IBM Plex Sans","fontPostScriptName":"IBMPlexSans-SemiBold","fontStyle":"SemiBold","textCase":"TITLE","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":24.0,"textAlignHorizontal":"LEFT","textAlignVertical":"CENTER","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":31.1999988555908,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"10:254":{"type":"TEXT","id":"10:254","name":"Refresh the page if the widget doesn’t load","absoluteBoundingBox":{"x":5244.5,"y":213.0,"width":250.0,"height":16.0},"isolatedAbsoluteRenderBounds":{"x":5245.48388671875,"y":217.0,"width":247.953125,"height":11.5440063476562},"relativeTransform":[[1.0,0.0,62.5],[0.0,1.0,113.0]],"size":{"x":250.0,"y":16.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.292217552661896,"g":0.292217552661896,"b":0.292217552661896,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Refresh the page if the widget doesn’t load","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"resolvedParagraphValues":{"0":{"listSpacing":0.0,"paragraphIndent":0.0,"paragraphSpacing":0.0}},"textAutoResize":"WIDTH_AND_HEIGHT","textAlignVertical":"CENTER","style":{"fontFamily":"IBM Plex Sans","fontPostScriptName":"IBMPlexSans-SemiBold","fontStyle":"SemiBold","textCase":"TITLE","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":12.0,"textAlignHorizontal":"LEFT","textAlignVertical":"CENTER","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":15.5999994277954,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"8:223":{"type":"FRAME","id":"8:223","name":"Button","absoluteBoundingBox":{"x":5202.0,"y":120.0,"width":53.0,"height":53.0},"isolatedAbsoluteRenderBounds":{"x":5202.0,"y":120.0,"width":53.0,"height":53.0},"relativeTransform":[[1.0,0.0,0.0],[0.0,1.0,0.0]],"size":{"x":53.0,"y":53.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":0.564705908298492,"b":0.800000011920929,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[{"id":{"sessionID":2,"localID":162},"event":{"interactionType":"ON_CLICK"},"actions":[{"transitionNodeID":{"sessionID":0,"localID":3},"connectionType":"INTERNAL_NODE","navigationType":"NAVIGATE","connectionURL":"/"}],"isDeleted":false,"stateManagementVersion":1}],"rectangleCornerRadii":[200.0,200.0,200.0,200.0],"cornerRadius":200.0,"paddingTop":10.0,"paddingRight":20.0,"paddingBottom":12.0,"paddingLeft":20.0,"clipsContent":true,"layoutMode":"HORIZONTAL","itemSpacing":20.0,"counterAxisAlignItems":"CENTER","primaryAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","counterAxisSizingMode":"FIXED","children":["8:224","8:225"]},"8:219":{"type":"TEXT","id":"8:219","name":"Home","absoluteBoundingBox":{"x":3878.0,"y":130.0,"width":66.0,"height":31.0},"isolatedAbsoluteRenderBounds":{"x":3879.96801757812,"y":138.248001098633,"width":62.566162109375,"height":17.0399932861328},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,10.0]],"size":{"x":66.0,"y":31.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.123685397207737,"g":0.123685397207737,"b":0.123685397207737,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Home","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"resolvedParagraphValues":{"0":{"listSpacing":0.0,"paragraphIndent":0.0,"paragraphSpacing":0.0}},"textAutoResize":"WIDTH_AND_HEIGHT","textAlignVertical":"CENTER","style":{"fontFamily":"IBM Plex Sans","fontPostScriptName":"IBMPlexSans-SemiBold","fontStyle":"SemiBold","textCase":"TITLE","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":24.0,"textAlignHorizontal":"LEFT","textAlignVertical":"CENTER","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":31.1999988555908,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"8:224":{"type":"TEXT","id":"8:224","name":"Home","absoluteBoundingBox":{"x":5222.0,"y":130.0,"width":66.0,"height":31.0},"isolatedAbsoluteRenderBounds":{"x":5222.0,"y":130.0,"width":66.0,"height":31.0},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,10.0]],"size":{"x":66.0,"y":31.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.123685397207737,"g":0.123685397207737,"b":0.123685397207737,"a":1.0},"visible":true,"opacity":1.0}],"visible":false,"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Home","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"resolvedParagraphValues":{"0":{"listSpacing":0.0,"paragraphIndent":0.0,"paragraphSpacing":0.0}},"textAutoResize":"WIDTH_AND_HEIGHT","textAlignVertical":"CENTER","style":{"fontFamily":"IBM Plex Sans","fontPostScriptName":"IBMPlexSans-SemiBold","fontStyle":"SemiBold","textCase":"TITLE","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":24.0,"textAlignHorizontal":"LEFT","textAlignVertical":"CENTER","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":31.1999988555908,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"7:140":{"type":"FRAME","id":"7:140","name":"Mobile","absoluteBoundingBox":{"x":5182.0,"y":100.0,"width":375.0,"height":1032.0},"isolatedAbsoluteRenderBounds":{"x":5182.0,"y":100.0,"width":375.0,"height":1032.0},"relativeTransform":[[1.0,0.0,1408.0],[0.0,1.0,100.0]],"size":{"x":375.0,"y":1032.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.123685397207737,"g":0.123685397207737,"b":0.123685397207737,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"paddingTop":20.0,"paddingRight":20.0,"paddingBottom":20.0,"paddingLeft":20.0,"clipsContent":true,"overflowDirection":"VERTICAL_SCROLLING","layoutMode":"VERTICAL","itemSpacing":40.0,"counterAxisAlignItems":"CENTER","counterAxisSizingMode":"FIXED","isBreakpointFrame":true,"children":["7:141","10:254","7:149"]},"7:136":{"type":"WIDGET","id":"7:136","name":"Embed 1","absoluteBoundingBox":{"x":3858.0,"y":274.0,"width":1240.0,"height":623.0},"isolatedAbsoluteRenderBounds":{"x":3858.0,"y":274.0,"width":1240.0,"height":623.0},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,174.0]],"size":{"x":1240.0,"y":623.0},"layoutAlign":"STRETCH","accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"syncedState":{"embedAllowFullscreen":"false","embedCodeType":"html","embedIframeHtml":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Flashcards</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n<style>\n  :root {\n    --bg:        #202020;\n    --surface:   #2C2C2C;\n    --text:      #F8F8F8;\n    --text-dim:  #A0A0A0;\n    --text-mute: #6E6E6E;\n    --line:      #3A3A3A;\n\n    --yellow:    #EBF400;\n    --green:     #00D45D;\n    --orange:    #FF7A18;\n    --blue:      #6A89FF;\n    --pink:      #FF90CC;\n    --red:       #FF4C56;\n    --sky:       #8FD0FF;\n\n    --ink:       #151515;   /* text that sits on a colour fill */\n    --accent:    var(--orange);\n    --radius:    999px;\n  }\n\n  * { box-sizing: border-box; }\n  body {\n    margin: 0; padding: 16px;\n    font-family: \"IBM Plex Sans\", ui-sans-serif, -apple-system, sans-serif;\n    color: var(--text); background: var(--bg);\n    -webkit-font-smoothing: antialiased;\n  }\n  .wrap { max-width: 820px; margin: 0 auto; }\n\n  .bar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; }\n  .bar.controls { margin-bottom: 12px; }\n  .spacer { flex: 1 1 auto; }\n\n  /* Pills */\n  button, select {\n    font: inherit; font-size: 14px; font-weight: 600;\n    padding: 11px 22px; border-radius: var(--radius);\n    border: none; background: var(--surface); color: var(--text);\n    cursor: pointer; transition: transform .1s ease, filter .1s ease;\n    -webkit-appearance: none; appearance: none;\n  }\n  select {\n    padding-right: 38px;\n    background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23F8F8F8' stroke-width='1.8' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\");\n    background-repeat: no-repeat; background-position: right 16px center;\n  }\n  button:hover, select:hover { filter: brightness(1.25); }\n  button:active { transform: scale(.97); }\n  button.primary { background: var(--accent); color: var(--ink); }\n  button.primary:hover { filter: brightness(1.08); }\n\n  /* Card — the hero pill */\n  .card {\n    background: var(--accent); color: var(--ink);\n    border-radius: 40px; padding: 48px 36px;\n    min-height: 320px;                  /* both faces share one height — no jump on flip */\n    display: flex; flex-direction: column; align-items: center; justify-content: center;\n    text-align: center; cursor: pointer; user-select: none;\n    transition: filter .12s ease;\n  }\n  .card:hover { filter: brightness(1.06); }\n  .card-stage { perspective: 1400px; min-height: 320px; }\n  body { overflow-x: hidden; }\n  .card { transform-style: preserve-3d; will-change: transform; }\n  @media (prefers-reduced-motion: reduce) {\n    .card { transition: filter .12s ease !important; }\n  }\n  .card.flip { background: var(--green); }\n\n  .hint {\n    font-family: \"IBM Plex Mono\", ui-monospace, monospace;\n    font-size: 11px; text-transform: uppercase; letter-spacing: .12em;\n    color: var(--ink); opacity: .55; margin-bottom: 20px; font-weight: 500;\n  }\n  .q { font-size: 26px; font-weight: 700; line-height: 1.25; letter-spacing: -0.02em; max-width: 640px; }\n  .a { font-size: 20px; font-weight: 500; line-height: 1.45; max-width: 640px; }\n\n  .meta { display: flex; gap: 8px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }\n  .tag {\n    font-family: \"IBM Plex Mono\", monospace;\n    font-size: 11px; font-weight: 500; padding: 5px 12px;\n    border-radius: var(--radius); background: rgba(21,21,21,.14); color: var(--ink);\n  }\n\n  .nav { display: flex; gap: 12px; align-items: center; justify-content: center; margin-top: 18px; }\n  .prog {\n    font-family: \"IBM Plex Mono\", monospace; font-size: 12px;\n    color: var(--text-mute); min-width: 74px; text-align: center;\n  }\n  .stats {\n    display: flex; gap: 16px; justify-content: center; margin-top: 14px;\n    font-family: \"IBM Plex Mono\", monospace; font-size: 12px; color: var(--text-mute);\n  }\n  .known { color: var(--green); } .again { color: var(--orange); }\n\n  .state { text-align: center; color: var(--text-mute); padding: 56px 20px; font-size: 15px; line-height: 1.6; }\n  .err { color: var(--red); font-size: 14px; line-height: 1.5; }\n\n  .setup { background: var(--surface); border-radius: 32px; padding: 36px 32px; }\n  .setup .lead { margin: 0 0 16px; font-size: 20px; font-weight: 700; letter-spacing: -0.02em; }\n  .setup input {\n    width: 100%; font: inherit; font-size: 14px; padding: 13px 20px;\n    border: none; border-radius: var(--radius);\n    background: var(--bg); color: var(--text);\n  }\n  .setup textarea {\n    width: 100%; font-family: \"IBM Plex Mono\", monospace; font-size: 13px; line-height: 1.6;\n    padding: 16px 20px; border: none; border-radius: 20px;\n    background: var(--bg); color: var(--text); resize: vertical;\n  }\n  .setup textarea:focus { outline: 2px solid var(--accent); }\n  .setup input::placeholder, .setup textarea::placeholder { color: var(--text-mute); }\n  .setup input:focus { outline: 2px solid var(--accent); outline-offset: 0; }\n  .help { font-size: 13px; line-height: 1.65; color: var(--text-mute); margin: 18px 0 0; }\n  .note {\n    margin-top: 22px; padding: 18px 20px; border-radius: 20px;\n    background: var(--bg); border: 1px dashed var(--line);\n  }\n  .note-head {\n    display: flex; gap: 14px; align-items: center; justify-content: space-between; flex-wrap: wrap;\n    font-size: 14px; font-weight: 600; color: var(--text);\n  }\n  .note-head button { font-size: 13px; padding: 8px 16px; background: var(--accent); color: var(--ink); }\n  .note .help { margin: 0; }\n  .help b { color: var(--text-dim); font-weight: 600; }\n\n  .list {\n    display: flex; flex-direction: column; gap: 8px;\n    max-height: 70vh; overflow-y: auto;\n    padding: 0 6px 8px 0;\n    overscroll-behavior: contain;\n  }\n  .list::-webkit-scrollbar { width: 8px; }\n  .list::-webkit-scrollbar-track { background: transparent; }\n  .list::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }\n  .list::-webkit-scrollbar-thumb:hover { background: var(--text-mute); }\n  .row {\n    display: flex; gap: 14px; align-items: flex-start;\n    background: var(--surface); border-radius: 20px; padding: 16px 22px; font-size: 14px;\n  }\n  .rq { flex: 1; font-weight: 600; color: var(--text); }\n  .ra { flex: 1; color: var(--text-dim); font-weight: 400; }\n  /* On a dark row the default ink-on-translucent tag is illegible — give it a real fill. */\n  .row .tag { background: transparent; color: var(--text-dim); border: 1px solid var(--line); flex: 0 0 auto; }\n</style>\n</head>\n<body>\n<div class=\"wrap\" id=\"app\"><div class=\"state\">Loading cards…</div></div>\n\n<script>\nconst STORE_KEY  = \"flashcards.progress.v1\";  // review stats, per device\nconst CACHE_KEY  = \"flashcards.cache.v1\";     // the deck itself, per device\n\nconst $ = (h) => { document.getElementById(\"app\").innerHTML = h; };\n\n/* If anything throws at parse/run time, say so instead of hanging on \"Loading…\" */\nwindow.onerror = function (msg, src, line) {\n  try {\n    document.getElementById(\"app\").innerHTML =\n      '<div class=\"state err\">Script error: ' + msg + ' (line ' + line + ')</div>';\n  } catch (e) {}\n};\nconst esc = (s) => String(s).replace(/[&<>\"]/g, c => ({ \"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\" }[c]));\n\n/* --- tiny CSV parser: handles quoted fields, commas, newlines --- */\nfunction parseCSV(text) {\n  const rows = []; let row = [], field = \"\", q = false;\n  for (let i = 0; i < text.length; i++) {\n    const c = text[i];\n    if (q) {\n      if (c === '\"') { if (text[i+1] === '\"') { field += '\"'; i++; } else q = false; }\n      else field += c;\n    } else if (c === '\"') q = true;\n    else if (c === \",\") { row.push(field); field = \"\"; }\n    else if (c === \"\\n\") { row.push(field); rows.push(row); row = []; field = \"\"; }\n    else if (c !== \"\\r\") field += c;\n  }\n  if (field.length || row.length) { row.push(field); rows.push(row); }\n  return rows.filter(r => r.some(v => v.trim() !== \"\"));\n}\n\n\n/* Parse pasted text into cards. Accepts, in order of preference:\n     1. CSV / TSV with a question,answer,category header (paste straight from a sheet)\n     2. Q: ... / A: ... / Cat: ... blocks\n     3. One card per line:  question | answer | category\n   Blank lines separate cards in format 2. */\nfunction parsePasted(text) {\n  const t = (text || \"\").trim();\n  if (!t) return [];\n\n  // 1. Sheet paste — tab or comma separated, with a header row\n  const firstLine = t.split(\"\\n\")[0].toLowerCase();\n  if (firstLine.includes(\"question\") && firstLine.includes(\"answer\")) {\n    const isTab = firstLine.includes(\"\\t\");\n    const rows = isTab\n      ? t.split(\"\\n\").map(r => r.split(\"\\t\"))\n      : parseCSV(t);\n    try { return rowsToCards(rows); } catch (e) { /* fall through */ }\n  }\n\n  // 3. Pipe-delimited, one per line\n  if (t.split(\"\\n\").every(l => !l.trim() || l.includes(\"|\"))) {\n    const out = t.split(\"\\n\").filter(l => l.trim()).map((l, n) => {\n      const p = l.split(\"|\").map(s => s.trim());\n      return { id: \"p\" + n, q: p[0] || \"\", a: p[1] || \"\", cat: p[2] || \"Uncategorized\" };\n    }).filter(c => c.q && c.a);\n    if (out.length) return out;\n  }\n\n  // 2. Q:/A: blocks\n  const out = [];\n  let cur = null, n = 0;\n  for (const raw of t.split(\"\\n\")) {\n    const line = raw.trim();\n    if (!line) { continue; }\n    const mq = line.match(/^(?:\\d+[.)]\\s*)?(?:q|question)\\s*[:.\\-]\\s*(.+)$/i);\n    const ma = line.match(/^(?:a|answer)\\s*[:.\\-]\\s*(.+)$/i);\n    const mc = line.match(/^(?:cat|category)\\s*[:.\\-]\\s*(.+)$/i);\n    if (mq) {\n      if (cur && cur.q && cur.a) out.push(cur);\n      cur = { id: \"p\" + (n++), q: mq[1].trim(), a: \"\", cat: \"Uncategorized\" };\n    } else if (ma && cur) {\n      cur.a = ma[1].trim();\n    } else if (mc && cur) {\n      cur.cat = mc[1].trim();\n    } else if (cur && cur.a) {\n      cur.a += \" \" + line;          // wrapped answer line\n    }\n  }\n  if (cur && cur.q && cur.a) out.push(cur);\n  return out;\n}\n\nfunction rowsToCards(rows) {\n  const head = rows[0].map(h => h.trim().toLowerCase());\n  const iq = head.indexOf(\"question\");\n  const ia = head.indexOf(\"answer\");\n  const ic = head.indexOf(\"category\");\n  if (iq === -1 || ia === -1) throw new Error('Sheet needs \"question\" and \"answer\" columns.');\n  return rows.slice(1).map((r, n) => ({\n    id: \"c\" + n,\n    q: (r[iq] || \"\").trim(),\n    a: (r[ia] || \"\").trim(),\n    cat: (ic > -1 && r[ic] ? r[ic].trim() : \"Uncategorized\"),\n  })).filter(c => c.q);\n}\n\n/* --- progress: localStorage, works fine on a real website --- */\nfunction loadProgress() {\n  try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {}; } catch { return {}; }\n}\nfunction saveProgress(p) {\n  try { localStorage.setItem(STORE_KEY, JSON.stringify(p)); } catch {}\n}\n\nlet CARDS = [], PROGRESS = loadProgress();\n\n/* Colour flicks through the palette as you move card to card. */\nconst PALETTE = [\"#FF7A18\", \"#00D45D\", \"#FF90CC\", \"#6A89FF\", \"#EBF400\", \"#8FD0FF\", \"#FF4C56\"];\nconst cardColor = (n) => PALETTE[((n % PALETTE.length) + PALETTE.length) % PALETTE.length];\nlet view = \"review\", cat = \"All\", order = \"newest\", idx = 0, flipped = false;\n\nfunction deck() {\n  let d = CARDS.filter(c => cat === \"All\" || c.cat === cat);\n  if (order === \"oldest\") d = [...d].reverse();\n  if (order === \"shuffle\") d = [...d].sort(() => Math.random() - 0.5);\n  if (order === \"weakest\") d = [...d].sort((a, b) => score(b) - score(a));\n  return d;\n}\nconst stat = (c) => PROGRESS[c.q] || { known: 0, again: 0 };\nconst score = (c) => stat(c).again - stat(c).known;\n\nfunction mark(field) {\n  const c = deck()[idx]; if (!c) return;\n  const s = stat(c);\n  PROGRESS[c.q] = { known: s.known + (field === \"known\" ? 1 : 0), again: s.again + (field === \"again\" ? 1 : 0) };\n  saveProgress(PROGRESS);\n  flipped = false; idx = (idx + 1) % Math.max(deck().length, 1);\n  render();\n}\n\nlet FLIPPING = false;\nfunction doFlip() {\n  if (FLIPPING) return;\n  const el = document.querySelector(\".card\");\n  if (!el) { flipped = !flipped; render(); return; }\n  if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n    flipped = !flipped; render(); return;\n  }\n  FLIPPING = true;\n  el.style.transition = \"transform .16s ease-in\";\n  el.style.transform  = \"rotateY(90deg)\";\n  setTimeout(() => {\n    flipped = !flipped;\n    render();                                   // repaint the new face while side-on\n    const next = document.querySelector(\".card\");\n    if (next) {\n      next.style.transition = \"none\";\n      next.style.transform  = \"rotateY(-90deg)\";\n      requestAnimationFrame(() => {\n        next.style.transition = \"transform .18s ease-out\";\n        next.style.transform  = \"rotateY(0deg)\";\n      });\n    }\n    FLIPPING = false;\n  }, 160);\n}\n\nfunction render() {\n  const cats = [\"All\", ...[...new Set(CARDS.map(c => c.cat))].sort()];\n  const d = deck();\n  const c = d[idx];\n  const s = c ? stat(c) : { known: 0, again: 0 };\n\n  let body;\n  if (!c) {\n    body = `<div class=\"state\">No cards in this category.</div>`;\n  } else if (view === \"review\") {\n    body = `\n      <div class=\"card-stage\"><div class=\"card\" style=\"background:${flipped ? \"var(--surface)\" : cardColor(idx)};color:${flipped ? \"var(--text)\" : \"var(--ink)\"}\" onclick=\"doFlip()\">\n        <div class=\"hint\" style=\"color:${flipped ? \"var(--text-mute)\" : \"var(--ink)\"};opacity:${flipped ? 1 : .55}\">${flipped ? \"Answer\" : \"Question — tap to flip\"}</div>\n        <div class=\"${flipped ? \"a\" : \"q\"}\">${esc(flipped ? c.a : c.q)}</div>\n        <div class=\"meta\">\n          <span class=\"tag\" style=\"background:${flipped ? cardColor(idx) : \"rgba(21,21,21,.14)\"};color:var(--ink)\">${esc(c.cat)}</span>\n        </div>\n      </div></div>\n      <div class=\"nav\">\n        <button onclick=\"flipped=false;idx=(idx-1+${d.length})%${d.length};render()\">←</button>\n        <span class=\"prog\">${idx + 1} / ${d.length}</span>\n        <button onclick=\"flipped=false;idx=(idx+1)%${d.length};render()\">→</button>\n      </div>\n      <div class=\"nav\" style=\"margin-top:10px\">\n        <button onclick=\"mark('again')\">Again</button>\n        <button class=\"primary\" onclick=\"mark('known')\">Got it</button>\n      </div>\n      <div class=\"stats\">\n        <span class=\"known\">Got it: ${s.known}</span>\n        <span class=\"again\">Again: ${s.again}</span>\n      </div>`;\n  } else {\n    body = `<div class=\"list\">${d.map((x, i) => `\n      <div class=\"row\">\n        <div class=\"rq\">${esc(x.q)}</div>\n        <div class=\"ra\">${esc(x.a)}</div>\n        <span class=\"tag\" style=\"border-color:${cardColor(i)};color:${cardColor(i)}\">${esc(x.cat)}</span>\n      </div>`).join(\"\")}</div>`;\n  }\n\n  $(`\n    <div class=\"bar controls\">\n      <button class=\"${view === \"review\" ? \"primary\" : \"\"}\" onclick=\"view='review';render()\">Review</button>\n      <button class=\"${view === \"browse\" ? \"primary\" : \"\"}\" onclick=\"view='browse';render()\">Browse (${CARDS.length})</button>\n    </div>\n    <div class=\"bar\">\n      <select onchange=\"cat=this.value;idx=0;flipped=false;render()\">\n        ${cats.map(x => `<option ${x === cat ? \"selected\" : \"\"}>${esc(x)}</option>`).join(\"\")}\n      </select>\n      <select onchange=\"order=this.value;idx=0;flipped=false;render()\">\n        <option value=\"newest\" ${order===\"newest\"?\"selected\":\"\"}>Newest first</option>\n        <option value=\"oldest\" ${order===\"oldest\"?\"selected\":\"\"}>Oldest first</option>\n        <option value=\"shuffle\" ${order===\"shuffle\"?\"selected\":\"\"}>Shuffle</option>\n        <option value=\"weakest\" ${order===\"weakest\"?\"selected\":\"\"}>Weakest first</option>\n      </select>\n      <span class=\"spacer\"></span>\n      <button onclick=\"setupScreen('')\">New cards</button>\n      <button onclick=\"if(confirm('Reset your review stats on this device?')){PROGRESS={};saveProgress({});render()}\">Reset</button>\n    </div>\n    ${body}\n  `);\n}\n\n\nconst LLM_PROMPT = `Turn the content below into flashcards.\n\nRules:\n- One fact per card. If a sentence holds two facts, split it into two cards.\n- The question must be a real question, never answerable with yes/no.\n- The answer must be the shortest complete answer — ideally under 20 words.\n- Skip filler, narrative and opinion. Only what is worth memorising.\n- Group the cards into a few sensible categories you choose from the content.\n\nReturn ONLY cards in exactly this format, nothing else:\n\nQ: <question>\nA: <answer>\nCat: <category>\n\nQ: <question>\nA: <answer>\nCat: <category>\n\nHere is the content:\n`;\n\nfunction setupScreen(errMsg) {\n  $(`\n    <div class=\"setup\">\n      <p class=\"lead\">Paste your cards.</p>\n      <textarea id=\"pasted\" rows=\"9\" placeholder=\"Q: What is a tripwire?\nA: A pre-agreed condition that reopens the plan.\nCat: Deciding\n\nQ: Who does a unanimity rule reward?\nA: Whoever digs in hardest.\nCat: Deciding\"></textarea>\n      <div class=\"bar\" style=\"margin-top:14px\">\n        <button class=\"primary\" onclick=\"applyPasted('add')\">Add to deck</button>\n        <button onclick=\"applyPasted('replace')\">Replace deck</button>\n        ${CARDS.length ? `<span class=\"prog\" style=\"min-width:auto\">${CARDS.length} cards in deck</span>` : \"\"}\n      </div>\n      ${errMsg ? `<p class=\"err\">${esc(errMsg)}</p>` : \"\"}\n\n      <div class=\"note\">\n        <div class=\"note-head\">\n          <span>Got raw material instead? Turn it into cards with any LLM.</span>\n          <button id=\"copybtn\" onclick=\"copyPrompt()\">Copy prompt</button>\n        </div>\n        <p class=\"help\" style=\"margin-top:10px\">\n          Copy the prompt, paste it into ChatGPT / Claude / anything else along with your\n          article, notes or transcript, then paste the result back in the box above.\n        </p>\n      </div>\n\n      <p class=\"help\">\n        Cards also accept <b>question | answer | category</b> per line, or rows pasted\n        straight out of a spreadsheet. Your deck is saved on this device.\n      </p>\n    </div>\n  `);\n}\n\nfunction copyPrompt() {\n  const btn = document.getElementById(\"copybtn\");\n  const done = () => { if (btn) { btn.textContent = \"Copied\"; setTimeout(() => { btn.textContent = \"Copy prompt\"; }, 1600); } };\n  if (navigator.clipboard && navigator.clipboard.writeText) {\n    navigator.clipboard.writeText(LLM_PROMPT).then(done, fallback);\n  } else fallback();\n  function fallback() {\n    const ta = document.createElement(\"textarea\");\n    ta.value = LLM_PROMPT;\n    ta.style.position = \"fixed\"; ta.style.opacity = \"0\";\n    document.body.appendChild(ta); ta.select();\n    try { document.execCommand(\"copy\"); done(); } catch (e) {}\n    document.body.removeChild(ta);\n  }\n}\n\nfunction applyPasted(mode) {\n  const el = document.getElementById(\"pasted\");\n  const incoming = parsePasted(el ? el.value : \"\");\n  if (!incoming.length) {\n    setupScreen(\"Couldn't find any cards in that. Check the format below.\");\n    return;\n  }\n\n  if (mode === \"replace\") {\n    CARDS = incoming;\n    // Stats are keyed on the question, so old ones would linger. Drop them.\n    const keep = new Set(incoming.map(c => c.q));\n    PROGRESS = Object.fromEntries(Object.entries(PROGRESS).filter(([q]) => keep.has(q)));\n    saveProgress(PROGRESS);\n  } else {\n    // Add on top, skipping any question that's already in the deck.\n    const seen = new Set(CARDS.map(c => c.q.trim().toLowerCase()));\n    const fresh = incoming.filter(c => !seen.has(c.q.trim().toLowerCase()));\n    CARDS = [...CARDS, ...fresh];\n  }\n  CARDS = CARDS.map((c, n) => ({ ...c, id: \"c\" + n }));   // keep ids unique after a merge\n\n  try { localStorage.setItem(CACHE_KEY, JSON.stringify(CARDS)); } catch (e) {}\n  idx = 0; flipped = false; cat = \"All\";\n  render();\n}\n\n\n\nfunction boot() {\n  let saved = null;\n  try { saved = JSON.parse(localStorage.getItem(CACHE_KEY)); } catch (e) {}\n  if (saved && saved.length) { CARDS = saved; render(); }\n  else setupScreen(\"\");\n}\n\nboot();\n</script>\n</body>\n</html>","embedURL":""}},"7:127":{"type":"FRAME","id":"7:127","name":"Desktop","absoluteBoundingBox":{"x":3838.0,"y":100.0,"width":1280.0,"height":817.0},"isolatedAbsoluteRenderBounds":{"x":3838.0,"y":100.0,"width":1280.0,"height":817.0},"relativeTransform":[[1.0,0.0,64.0],[0.0,1.0,100.0]],"size":{"x":1280.0,"y":817.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.123685397207737,"g":0.123685397207737,"b":0.123685397207737,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"paddingTop":20.0,"paddingRight":20.0,"paddingBottom":20.0,"paddingLeft":20.0,"clipsContent":true,"overflowDirection":"VERTICAL_SCROLLING","layoutMode":"VERTICAL","itemSpacing":40.0,"counterAxisAlignItems":"CENTER","counterAxisSizingMode":"FIXED","isBreakpointFrame":true,"children":["7:128","10:253","7:136"]},"10:253":{"type":"TEXT","id":"10:253","name":"Refresh the page if the widget doesn’t load","absoluteBoundingBox":{"x":4319.5,"y":213.0,"width":317.0,"height":21.0},"isolatedAbsoluteRenderBounds":{"x":4320.81201171875,"y":218.0,"width":314.22900390625,"height":15.3919982910156},"relativeTransform":[[1.0,0.0,481.5],[0.0,1.0,113.0]],"size":{"x":317.0,"y":21.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.292217552661896,"g":0.292217552661896,"b":0.292217552661896,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Refresh the page if the widget doesn’t load","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"resolvedParagraphValues":{"0":{"listSpacing":0.0,"paragraphIndent":0.0,"paragraphSpacing":0.0}},"textAutoResize":"WIDTH_AND_HEIGHT","textAlignVertical":"CENTER","style":{"fontFamily":"IBM Plex Sans","fontPostScriptName":"IBMPlexSans-SemiBold","fontStyle":"SemiBold","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":16.0,"textAlignHorizontal":"LEFT","textAlignVertical":"CENTER","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":20.7999992370605,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textCase":"ORIGINAL","textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"8:220":{"type":"SVG","id":"8:220","name":"Group 1","absoluteBoundingBox":{"x":3963.99991120312,"y":135.600499896467,"width":19.7990057187671,"height":19.7990057187676},"targetAspectRatio":{"x":86.0000076293945,"y":86.0000076293945},"isolatedAbsoluteRenderBounds":{"x":3962.58569335938,"y":134.893417358398,"width":21.2132235625086,"height":21.2132110595703},"relativeTransform":[[-0.70710676908493,0.70710676908493,115.899505615234],[-0.70710676908493,-0.70710676908493,35.3995018005371]],"size":{"x":14.0000114440918,"y":14.0000114440918},"fills":[],"strokeAlign":"INSIDE","strokes":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"hash":"798e3de9ce02b7650d7beb69e773d4cf9057e8be"},"7:130":{"type":"TEXT","id":"7:130","name":"Flashcards","absoluteBoundingBox":{"x":4033.79907226562,"y":130.0,"width":122.0,"height":31.0},"isolatedAbsoluteRenderBounds":{"x":4035.76708984375,"y":137.240005493164,"width":118.87646484375,"height":18.0479888916016},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,10.0]],"size":{"x":122.0,"y":31.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":0.123685397207737,"g":0.123685397207737,"b":0.123685397207737,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"OUTSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"characterStyleOverrides":[],"characters":"Flashcards","lineIndentations":[0],"lineTypes":["NONE"],"listStartOffsets":[],"lineStyleOverrides":[0],"lineTextDirections":null,"resolvedParagraphValues":{"0":{"listSpacing":0.0,"paragraphIndent":0.0,"paragraphSpacing":0.0}},"textAutoResize":"WIDTH_AND_HEIGHT","textAlignVertical":"CENTER","style":{"fontFamily":"IBM Plex Sans","fontPostScriptName":"IBMPlexSans-SemiBold","fontStyle":"SemiBold","textCase":"TITLE","textAutoResize":"WIDTH_AND_HEIGHT","fontVariantPosition":"NORMAL","fontSize":24.0,"textAlignHorizontal":"LEFT","textAlignVertical":"CENTER","letterSpacing":0.0,"letterSpacingValue":0.0,"letterSpacingUnit":"PERCENT","lineHeightPx":31.1999988555908,"lineHeightPercent":100.0,"lineHeightUnit":"INTRINSIC_%","paragraphSpacing":0,"paragraphIndent":0,"listSpacing":0,"italic":false,"textDecoration":"NONE","textDecorationSkipInk":false,"textDecorationStyle":"solid","textTruncation":"DISABLED","lineHeightPercentFontSize":100},"styleOverrideTable":{}},"7:141":{"type":"FRAME","id":"7:141","name":"Header","absoluteBoundingBox":{"x":5202.0,"y":120.0,"width":335.0,"height":53.0},"isolatedAbsoluteRenderBounds":{"x":5202.0,"y":120.0,"width":335.0,"height":53.0},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,20.0]],"size":{"x":335.0,"y":53.0},"fills":[],"strokeAlign":"INSIDE","layoutAlign":"STRETCH","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"layoutMode":"HORIZONTAL","itemSpacing":10.0,"counterAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","children":["8:223","7:142"]},"8:218":{"type":"FRAME","id":"8:218","name":"Button","absoluteBoundingBox":{"x":3858.0,"y":120.0,"width":145.799011230469,"height":53.0},"isolatedAbsoluteRenderBounds":{"x":3858.0,"y":120.0,"width":145.799072265625,"height":53.0},"relativeTransform":[[1.0,0.0,0.0],[0.0,1.0,0.0]],"size":{"x":145.799011230469,"y":53.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":0.564705908298492,"b":0.800000011920929,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[{"id":{"sessionID":2,"localID":163},"event":{"interactionType":"ON_CLICK"},"actions":[{"transitionNodeID":{"sessionID":0,"localID":3},"connectionType":"INTERNAL_NODE","navigationType":"NAVIGATE","connectionURL":"/"}],"isDeleted":false,"stateManagementVersion":1}],"rectangleCornerRadii":[200.0,200.0,200.0,200.0],"cornerRadius":200.0,"paddingTop":10.0,"paddingRight":20.0,"paddingBottom":12.0,"paddingLeft":20.0,"clipsContent":true,"layoutMode":"HORIZONTAL","itemSpacing":20.0,"counterAxisAlignItems":"CENTER","children":["8:219","8:220"]},"7:142":{"type":"FRAME","id":"7:142","name":"Button","absoluteBoundingBox":{"x":5265.0,"y":120.0,"width":272.0,"height":53.0},"isolatedAbsoluteRenderBounds":{"x":5265.0,"y":120.0,"width":272.0,"height":53.0},"relativeTransform":[[1.0,0.0,63.0],[0.0,1.0,0.0]],"size":{"x":272.0,"y":53.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":0.47843137383461,"b":0.0941176488995552,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","layoutGrow":1.0,"strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"rectangleCornerRadii":[200.0,200.0,200.0,200.0],"cornerRadius":200.0,"paddingTop":10.0,"paddingRight":20.0,"paddingBottom":12.0,"paddingLeft":20.0,"clipsContent":true,"layoutMode":"HORIZONTAL","itemSpacing":40.0,"counterAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","children":["7:143"]},"7:129":{"type":"FRAME","id":"7:129","name":"Button","absoluteBoundingBox":{"x":4013.79907226562,"y":120.0,"width":1084.20092773438,"height":53.0},"isolatedAbsoluteRenderBounds":{"x":4013.79907226562,"y":120.0,"width":1084.20092773438,"height":53.0},"relativeTransform":[[1.0,0.0,155.799011230469],[0.0,1.0,0.0]],"size":{"x":1084.20092773438,"y":53.0},"fills":[{"blendMode":"NORMAL","type":"SOLID","color":{"r":1.0,"g":0.47843137383461,"b":0.0941176488995552,"a":1.0},"visible":true,"opacity":1.0}],"strokeAlign":"INSIDE","layoutGrow":1.0,"strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"rectangleCornerRadii":[200.0,200.0,200.0,200.0],"cornerRadius":200.0,"paddingTop":10.0,"paddingRight":20.0,"paddingBottom":12.0,"paddingLeft":20.0,"clipsContent":true,"layoutMode":"HORIZONTAL","itemSpacing":40.0,"counterAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","children":["7:130"]},"8:225":{"type":"SVG","id":"8:225","name":"Group 1","absoluteBoundingBox":{"x":5217.18628495115,"y":134.186290605416,"width":22.6274300977093,"height":22.6274300977091},"targetAspectRatio":{"x":86.0000076293945,"y":86.0000076293945},"isolatedAbsoluteRenderBounds":{"x":5215.77197265625,"y":133.479202270508,"width":24.0417423926046,"height":24.0416412353516},"relativeTransform":[[-0.70710676908493,0.70710676908493,26.5],[-0.70710676908493,-0.70710676908493,36.8137168884277]],"size":{"x":16.0000095367432,"y":16.0000095367432},"fills":[],"strokeAlign":"INSIDE","strokes":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"hash":"691b3de63fc4d2eaea85228adcc8c006cffacce8"},"7:128":{"type":"FRAME","id":"7:128","name":"Header","absoluteBoundingBox":{"x":3858.0,"y":120.0,"width":1240.0,"height":53.0},"isolatedAbsoluteRenderBounds":{"x":3858.0,"y":120.0,"width":1240.0,"height":53.0},"relativeTransform":[[1.0,0.0,20.0],[0.0,1.0,20.0]],"size":{"x":1240.0,"y":53.0},"fills":[],"strokeAlign":"INSIDE","layoutAlign":"STRETCH","strokes":[],"effects":[],"accessibleHTMLTag":"AUTO","isDecorativeImage":false,"ariaAttributes":{},"interactions":[],"layoutMode":"HORIZONTAL","itemSpacing":10.0,"counterAxisAlignItems":"CENTER","primaryAxisSizingMode":"FIXED","children":["8:218","7:129"]}},"assetIdToGuid":{},"guidToUrl":{"2:2":"/fretboard","11:324":"/daily-spend","7:126":"/flashcards","0:3":"/"},"fonts":{"IBM Plex Sans:SemiBold":{"id":"IBMPlexSans_wdth_wght__1","url":"/_woff/v2/IBMPlexSans_wdth_wght__1/IBMPlexSans_wdth_wght__1.woff2","source":1,"italic":false,"weight":600,"variationAxes":[{"tag":"wght","value":600.0,"name":"Weight"},{"tag":"wdth","value":100.0,"name":"Width"}],"subsets":{"baseUrl":"/_woff/v2/IBMPlexSans_wdth_wght__1/","subsetMappings":[{"unicodeRange":"U+0000-00A0,U+00A2-00A9,U+00AC-00AE,U+00B0-00B7,U+00B9-00BA,U+00BC-00BE,U+00D7,U+00F7,U+2000-206F,U+2074,U+20AC,U+2122,U+2190-21BB,U+2212,U+2215,U+F8FF,U+FEFF,U+FFFD","file":"IBMPlexSans_wdth_wght__1-english.woff2"},{"unicodeRange":"U+00A1,U+00AA-00AB,U+00AF,U+00B8,U+00BB,U+00BF-00D6,U+00D8-00F6,U+00F8-00FF,U+0131,U+0152-0153,U+02B0-02FF","file":"IBMPlexSans_wdth_wght__1-rest-latin.woff2"},{"unicodeRange":"U+0100-0130,U+0132-0151,U+0154-017F","file":"IBMPlexSans_wdth_wght__1-latin-extended-a.woff2"},{"unicodeRange":"U+0180-024F","file":"IBMPlexSans_wdth_wght__1-latin-extended-b.woff2"},{"unicodeRange":"U+1E00-1EFF","file":"IBMPlexSans_wdth_wght__1-latin-extended-additional.woff2"},{"unicodeRange":"U+0250-02AF,U+0300-1DFF,U+1F00-1FFF,U+2070-2073,U+2075-20AB,U+20AD-2121,U+2123-218F,U+21BC-2211,U+2213-2214,U+2216-F8FE,U+F900-FEFE,U+FF00-FFFC,U+FFFE-FFFF","file":"IBMPlexSans_wdth_wght__1-rest.woff2"}]}}},"assets":{"691b3de63fc4d2eaea85228adcc8c006cffacce8":{"type":"GENERATED_ASSET","url":"691b3de63fc4d2eaea85228adcc8c006cffacce8.svg","size":{"x":17.7071132659912,"y":17.7071151733398},"offsets":{"left":{"value":-4.4194578859461,"unit":"PERCENT"},"right":{"value":-6.24993406240102,"unit":"PERCENT"},"top":{"value":-6.25004246828409,"unit":"PERCENT"},"bottom":{"value":-4.41936140098488,"unit":"PERCENT"}},"format":"SVG"},"798e3de9ce02b7650d7beb69e773d4cf9057e8be":{"type":"GENERATED_ASSET","url":"798e3de9ce02b7650d7beb69e773d4cf9057e8be.svg","size":{"x":15.7071142196655,"y":15.7071151733398},"offsets":{"left":{"value":-5.0508164092611,"unit":"PERCENT"},"right":{"value":-7.14276487737529,"unit":"PERCENT"},"top":{"value":-7.14291516610677,"unit":"PERCENT"},"bottom":{"value":-5.05067293248346,"unit":"PERCENT"}},"format":"SVG"},"80b30a8b8d495f47f96c611b0b160ea7b7cf0546":{"type":"GENERATED_ASSET","url":"80b30a8b8d495f47f96c611b0b160ea7b7cf0546.png","size":{"x":1200.0,"y":630.0},"offsets":{"top":{"value":0,"unit":"PIXELS"},"right":{"value":0,"unit":"PIXELS"},"bottom":{"value":0,"unit":"PIXELS"},"left":{"value":0,"unit":"PIXELS"}},"format":"PNG"},"b46c3e4bd3e95a52c9b79f9891719f37e799a9a7":{"type":"GENERATED_ASSET","url":"b46c3e4bd3e95a52c9b79f9891719f37e799a9a7.png","size":{"x":95.0,"y":95.0},"offsets":{"top":{"value":0,"unit":"PIXELS"},"right":{"value":0,"unit":"PIXELS"},"bottom":{"value":0,"unit":"PIXELS"},"left":{"value":0,"unit":"PIXELS"}},"format":"PNG"}},"stablePathToAssetInfo":{"8:220":{"hash":"798e3de9ce02b7650d7beb69e773d4cf9057e8be"},"11:417":{"hash":"80b30a8b8d495f47f96c611b0b160ea7b7cf0546"},"8:225":{"hash":"691b3de63fc4d2eaea85228adcc8c006cffacce8"},"2:164":{"hash":"b46c3e4bd3e95a52c9b79f9891719f37e799a9a7"}},"animateRootIds":[],"siteSettings":{"title":"Useful Shit | Free Little Tools for Learning & Everyday Life","description":"A small collection of free browser tools made for fun: learn every note on the guitar fretboard, turn any content into flashcards, and track your daily spend.","scalingMode":"REFLOW","lang":"en","faviconFilename":"b46c3e4bd3e95a52c9b79f9891719f37e799a9a7.png","socialImageFilename":"80b30a8b8d495f47f96c611b0b160ea7b7cf0546.png","labs":{"E5FBBA911B2B7A09E649D4BE6CDF8591EAEFC881":false}},"sourceCodeHash":""}