Skip to content

Repository files navigation

title Knowbooks - experiment kits in a book, for humans of all ages.
description Knowbooks are experiment kits in a book, for humans of all ages.
baseUrl https://knowbooks.com/
permalink index.html
openGraphImage og.png
favicon knowbooksLogo.svg
buildHtml
metaTags

upstreamToScroll.scroll

buildToFileParser
 description Write the raw body to one or more files during the first build pass.
 extends abstractBuildOneCommandParser
 cueFromId
 baseParser blobParser
 example
  buildToFile robots.txt robots-copy.txt
   User-agent: *
   Disallow:
 javascript
  buildOutput() {
   return this.subparticlesToString()
  }
  async buildOne() {
   const { root } = this
   const { fileSystem, folderPath, filename, path } = root
   const content = this.buildOutput()
   for (const name of this.outputFileNames) {
    await fileSystem.writeProduct(path.join(folderPath, name), content)
    root.log(`💾 Built ${name} from ${filename}`)
   }
  }

buildZipParser
 description Zip the listed files or folders after preceding build commands. Requires the zip command.
 extends abstractBuildCommandParser
 cueFromId
 javascript
  async buildTwo() {
   const { folderPath, fileSystem, path } = this.root
   const [name, ...files] = this.getAtomsFrom(1)
   const zip = require("child_process").execFileSync("zip", ["-rq", "-", ...files], { cwd: folderPath, maxBuffer: 100 * 1024 * 1024 })
   await fileSystem.writeProduct(path.join(folderPath, name), zip)
  }

buildGitHashParser
 description Write the current git commit hash to gitHash.js so receipt links carry a version marker.
 cueFromId
 extends abstractBuildOneCommandParser
 baseParser blobParser
 javascript
  async buildOne() {
   const { root } = this
   const { fileSystem, folderPath, path } = root
   let hash = "unknown"
   try { hash = require("child_process").execFileSync("git", ["rev-parse", "--short=7", "HEAD"], { cwd: folderPath }).toString().trim() } catch {}
   await fileSystem.writeProduct(path.join(folderPath, "gitHash.js"), `const KNOWBOOKS_GIT_HASH = ${JSON.stringify(hash)};\n`)
   root.log(`💾 Built gitHash.js (${hash})`)
  }

buildGitHash gitHash.js

buildToFile knowbooksLogo.svg
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
  <title id="title">Knowbooks</title>
  <desc id="desc">A circular ring supported by two hands.</desc>
  <g fill="#d9bc78">
   <path fill-rule="evenodd" d="M128 14C68.45 14 20 62.45 20 122c0 18.12 4.5 35.2 12.42 50.2l12.37-7.15A93.28 93.28 0 0 1 34.3 122c0-51.67 42.03-93.7 93.7-93.7s93.7 42.03 93.7 93.7a93.28 93.28 0 0 1-10.49 43.05l12.37 7.15A107.42 107.42 0 0 0 236 122c0-59.55-48.45-108-108-108Z"/>
   <path d="M29.31 156.42c-4.34-7.23-3.66-13.77.07-15.84 3.38-1.87 7.33.31 10.53 4.4l22.82 25.16c1.12 1.24 2.85-.31 1.88-1.66l-19.1-26.67c-3.67-5.13-3.33-10.19.2-12.41 3.77-2.38 8.54.65 12.52 5.26l18.1 20.97c3.7 4.29 8.36 7.65 12.05 12.6 5.96 7.99 6.08 19.48 6.08 31.36v25.99c-24.1-2.86-43.02-12.51-53.85-26.48-7.38-9.52-8.76-23.53-11.3-42.68Z"/>
   <path d="M226.69 156.42c4.34-7.23 3.66-13.77-.07-15.84-3.38-1.87-7.33.31-10.53 4.4l-22.82 25.16c-1.12 1.24-2.85-.31-1.88-1.66l19.1-26.67c3.67-5.13 3.33-10.19-.2-12.41-3.77-2.38-8.54.65-12.52 5.26l-18.1 20.97c-3.7 4.29-8.36 7.65-12.05 12.6-5.96 7.99-6.08 19.48-6.08 31.36v25.99c24.1-2.86 43.02-12.51 53.85-26.48 7.38-9.52 8.76-23.53 11.3-42.68Z"/>
  </g>
 </svg>

css
 #designer { width: min(850px, calc(100% - 24px)); max-height: 96dvh; padding: 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); color: var(--ink); }
 #design-source { margin-top: 10px; font: 13px/1.5 Arial, sans-serif; }
 #design-source summary { cursor: pointer; }
 #design-link { width: 100%; margin-top: 6px; padding: 8px; border: 1px solid var(--line); border-radius: 4px; background: #fff8; font: 12px monospace; }
 #design-source label { display: block; margin-top: 12px; }
 #design-code { display: block; width: 100%; min-height: 240px; margin-top: 10px; padding: 12px; resize: vertical; font: 12px/1.5 monospace; border: 1px solid var(--line); border-radius: 4px; background: #fff8; color: var(--ink); }
 #designer::backdrop { background: #151d19aa; }
 .cover-drop-target { position: fixed; inset: 12px; z-index: 10000; display: grid; place-items: center; padding: 24px; border: 3px dashed #d9bc78; border-radius: 12px; background: #151d19e6; color: #f4e5bf; text-align: center; font: 600 clamp(20px, 4vw, 32px)/1.4 Arial, sans-serif; pointer-events: none; }
 .cover-drop-target[hidden] { display: none; }
 body:has(#designer[open]) { overflow: hidden; }
 .designer-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
 .designer-head h2 { margin: 0; font-size: 26px; font-weight: normal; }
 #design-close { border: 0; background: none; font-size: 28px; }
 .design-spread { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; }
 .design-cover { background: var(--cover, #294575); color: var(--gold); border-radius: 5px 12px 12px 5px; box-shadow: inset 8px 0 8px #0003, 5px 5px 0 #dcd1b9; padding: 16px 18px 12px; text-align: center; max-width: 320px; margin: auto; }
 #design-title { font: 24px/1.2 Georgia, serif; min-height: 29px; overflow-wrap: anywhere; margin: 0 0 10px; }
 .design-signature-wrap { position: relative; display: block; margin: -2px 0 6px; }
 #design-signature { display: block; width: 100%; padding: 3px; border: 0; background: transparent; color: inherit; text-align: center; font: 400 22px/1.15 Georgia, serif; }
 #design-signature::placeholder { color: inherit; opacity: .7; }
 #design-signature-art { position: absolute; inset: 0; display: block; padding: 3px; pointer-events: none; }
 #design-signature-art svg { display: block; width: 100%; height: 100%; }
 .design-signature-wrap:not(:focus-within) #design-signature:not(:placeholder-shown) { color: transparent; }
 .design-signature-wrap:focus-within #design-signature-art, .design-signature-wrap #design-signature:placeholder-shown ~ #design-signature-art { visibility: hidden; }
 #designer [hidden] { display: none; }
 [data-edit-design] { display: inline-flex; align-items: center; gap: 6px; padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; text-align: left; }
 [data-edit-design]:hover { text-decoration: underline; }
 .design-actions { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 6px 0; }
 .book-signature { display: block; margin-top: 5px; }
 .book-signature svg { display: block; width: 100%; height: 26px; }
 #design-title:focus { outline: 1px dashed var(--gold); }
 .design-cover svg { display: block; width: 100%; touch-action: none; cursor: crosshair; }
 #design-art { height: clamp(110px, 27dvh, 210px); }
 .design-cover .strokes { fill: none; stroke: currentColor; stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; pointer-events: stroke; }
 .design-cover > span { display: block; margin-top: 10px; font: 10px Arial, sans-serif; letter-spacing: .25em; }
 #design-colors { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0 6px; justify-content: center; }
 #design-colors button { width: 24px; height: 24px; border: 2px solid transparent; border-radius: 50%; background: var(--swatch); cursor: pointer; }
 #design-colors button[aria-pressed=true] { outline: 2px solid var(--ink); outline-offset: 2px; }
 .design-tools { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 4px 14px; font: 13px Arial, sans-serif; }
 .design-tools button { cursor: pointer; border: 0; background: none; padding: 8px 0; }
 #design-marker { display: flex; align-items: center; gap: 8px; }
 #design-marker[aria-pressed=true] svg { transform: rotate(180deg); }
 #designer small { display: block; font: 12px/1.5 Arial, sans-serif; color: var(--muted); }
 .design-upload { position: relative; cursor: pointer; padding: 8px 0; }
 #portrait-file { position: absolute; inset: 0; width: 100%; opacity: 0; cursor: pointer; }
 .design-upload:focus-within { outline: 2px solid var(--ink); outline-offset: 3px; }
 #design-marker svg { width: 64px; }
 .design-inside { font: 13px/1.4 Arial, sans-serif; }
 .design-inside p { margin: 10px 0; }
 #design-status:empty { display: none; }
 .design-inside h3 { font: 26px Georgia, serif; margin: 0; }
 #design-experiments input { display: block; margin: 4px 0; width: 100%; min-width: 0; padding: 6px; border: 1px solid var(--line); border-radius: 4px; background: #fff8; font: inherit; }
 .design-price { font: 30px Georgia, serif; margin: 14px 0 8px; }
 .design-price small { display: inline !important; }
 #design-add { width: 100%; padding: 12px; border: 0; border-radius: 5px; color: white; background: #243d32; font-weight: bold; cursor: pointer; }
 #design-status { font-size: 12px; }
 #design-code { height: min(52dvh, 440px); min-height: 120px; }
 @media(max-width: 600px) {
  #designer { padding: 12px; } .designer-head h2 { font-size: 21px; }
  .design-spread { gap: 6px 10px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .design-inside, .design-spread > div:first-child { display: contents; }
  .design-cover { grid-column: 1; grid-row: 1; width: 100%; }
  #design-colors { grid-column: 1 / -1; grid-row: 2; }
  .design-tools { grid-column: 1 / -1; grid-row: 3; }
  .design-inside h3 { grid-column: 2; grid-row: 1; align-self: start; font-size: 20px; }
  #design-experiments { grid-column: 2; grid-row: 1; }
  .design-inside > p, #design-add { grid-column: 1 / -1; margin: 0; }
  .design-cover { padding: 12px 8px 8px; } #design-title { font-size: 18px; margin-bottom: 4px; }
  #design-signature { font-size: 19px; } #design-art { height: 100px; }
  #design-colors { gap: 5px; margin: 8px 0 4px; } #design-colors button { width: 17px; height: 17px; }
  .design-tools { gap: 2px 8px; font-size: 11px; } #design-marker { gap: 3px; } #design-marker svg { width: 24px; }
 }
 :root {
  --paper: #f4efe5;
  --ink: #20211e;
  --muted: #706b61;
  --line: rgba(32, 33, 30, .18);
  --gold: #d9bc78;
  color-scheme: light;
  font-family: Georgia, "Times New Roman", serif;
 }
 * { box-sizing: border-box; }
 html { scroll-behavior: smooth; }
 body { margin: 0; color: var(--ink); background: var(--paper); }
 body.cart-open { overflow: hidden; }
 button, a { color: inherit; }
 button { font: inherit; }
 .site-header {
  position: absolute;
  z-index: 10;
  inset: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 24px clamp(20px, 5vw, 72px);
 }
 .brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; font: 700 11px/1 Arial, sans-serif; letter-spacing: .2em; }
 .brand img { width: 30px; height: 30px; padding: 5px; border-radius: 50%; background: var(--ink); }
 .faq-link { margin-left: auto; margin-right: 100px; text-decoration: none; text-transform: uppercase; font: 700 10px/1 Arial, sans-serif; letter-spacing: .13em; }
 .faq-link:hover, .faq-link:focus-visible { text-decoration: underline; text-underline-offset: 4px; }
 .cart-toggle { position: fixed; z-index: 25; top: 16px; right: clamp(16px, 4vw, 60px); width: 86px; height: 78px; padding: 0; border: 0; background: transparent; cursor: pointer; filter: drop-shadow(0 5px 5px #0002); transition: opacity 200ms, transform 200ms; }
 .corner-books { position: absolute; inset: 0 auto auto 0; width: 200px; height: 148px; transform: scale(.43, .527027); transform-origin: top left; pointer-events: none; }
 .cart-toggle > svg { position: relative; z-index: 2; }
 .cart-toggle .cart-count { z-index: 3; }
 .cart-toggle:hover { transform: rotate(-5deg); }
 .cart-toggle > svg, .cart-art > svg { width: 100%; height: 100%; overflow: visible; }
 .cart-count { position: absolute; top: 0; right: 0; display: grid; place-items: center; min-width: 25px; height: 25px; padding: 0 5px; border-radius: 50%; color: var(--paper); background: var(--ink); font: 700 11px Arial, sans-serif; }
 body.cart-open .cart-toggle, body.cart-travelling .cart-toggle { opacity: 0; pointer-events: none; }
 .cart-art { display: block; position: relative; width: 200px; height: 148px; margin: 0 auto 12px; flex-shrink: 0; }
 #cart .cart-art > svg { display: none; }
 #cart .cart-art:has(.basket-books:empty) { display: none; }
 .cart-art > svg { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
 .basket-books { position: absolute; inset: 8px 24px 43px 50px; display: flex; align-items: end; justify-content: center; }
 .basket-book { --mini-width: 48px; --book-scale: .177777778; flex-shrink: 0; margin: 0; transform: rotate(var(--tilt)); }
 .basket-book + .basket-book { margin-left: var(--pile-overlap, -18px); }
 .cart-flight, .flying-book { position: fixed; z-index: 50; pointer-events: none; margin: 0; transform-origin: center; }
 .cart-flight { display: block; z-index: 19; }
 .cart-flight.is-returning { z-index: 1000; }
 .gathering-book { position: fixed; z-index: 1001; pointer-events: none; }
 .cart-flight > svg { width: 100%; height: 100%; }
 .preview-add { flex-shrink: 0; width: 100%; display: flex; align-items: center; justify-content: center; gap: 10px; min-height: 44px; padding: 12px 8px; border: 1px solid #cbb57866; border-radius: 999px; background: #f4e5bf; color: #262b22; box-shadow: 0 4px 12px #65512714; cursor: pointer; font: 700 clamp(10px, 1.2vw, 14px) Arial, sans-serif; }
 .preview-add:hover { background: #fff2d4; }
 .preview-add:disabled { opacity: .6; cursor: wait; }
 .preview-add span { opacity: .65; font-weight: 400; }
 button:focus-visible, a:focus-visible { outline: 3px solid #b58132; outline-offset: 5px; }
 .hero {
  aspect-ratio: 1730 / 909;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: max(88px, 6vw) 5.6vw 0;
  position: relative;
  background: #eee4d8 url("hero-background.png") center / 100% 100% no-repeat;
 }
 .hero h1 { max-width: 46vw; font-size: 3.8vw; line-height: .95; }
 .hero .intro { max-width: 43vw; margin: 1vw 0; font-size: 1.15vw; line-height: 1.4; color: #353129; }
 .hero .down { font-size: .7vw; padding-bottom: .35vw; }
 @media (max-width: 1000px) {
  .hero { aspect-ratio: auto; padding: 110px 6vw calc(52.55vw + 28px); background-size: 100% auto; background-position: center bottom; }
  .hero h1 { max-width: 720px; font-size: clamp(48px, 8vw, 76px); }
  .hero .intro { max-width: 610px; margin: 24px 0; font-size: 18px; }
  .hero .down { font-size: 10px; padding-bottom: 7px; }
 }
 .eyebrow { margin: 0 0 20px; text-transform: uppercase; font: 700 10px/1 Arial, sans-serif; letter-spacing: .2em; }
 h1 { position: relative; z-index: 1; max-width: 820px; margin: 0; font-size: clamp(58px, 9vw, 132px); font-weight: 400; line-height: .85; letter-spacing: -.055em; }
 .intro { position: relative; z-index: 1; max-width: 610px; margin: 36px 0; color: #49473f; font-size: clamp(18px, 2vw, 25px); line-height: 1.45; }
 .down { position: relative; z-index: 1; display: inline-flex; gap: 18px; padding-bottom: 7px; border-bottom: 1px solid; text-decoration: none; text-transform: uppercase; font: 700 10px/1 Arial, sans-serif; letter-spacing: .15em; }
 .catalog-section { padding: 100px clamp(20px, 5vw, 72px) 120px; }
 .offering { padding: 72px clamp(20px, 8vw, 124px); border-top: 1px solid var(--line); }
 .offering:last-of-type { margin-bottom: 80px; }
 .offering .section-heading { margin-bottom: 32px; }
 .offering-body { max-width: 720px; margin-left: calc((100% + 30px) / 3); }
 .offering-body > p { margin: 0 0 24px; color: var(--muted); font: 17px/1.7 Arial, sans-serif; }
 .offering-body .offering-lead { color: var(--ink); font: clamp(22px, 3vw, 30px)/1.4 Georgia, "Times New Roman", serif; }
 #startups { background: #ebe3d3; }
 .offering-terms { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin: 32px 0; }
 .offering-terms div { padding-top: 18px; border-top: 1px solid var(--line); }
 .offering-terms dt { margin-bottom: 10px; font-size: 23px; }
 .offering-terms dd { margin: 0; color: var(--muted); font: 15px/1.6 Arial, sans-serif; }
 .offering .down { line-height: 1.5; }
 @media (max-width: 850px) { .offering-body { margin-left: 0; } }
 @media (max-width: 520px) { .offering-terms { grid-template-columns: 1fr; } }
 .section-heading { display: grid; grid-template-columns: 1fr 2fr; align-items: end; gap: 30px; margin-bottom: 60px; }
 .section-heading .eyebrow { align-self: start; padding-top: 11px; text-decoration: none; }
 .section-heading h2 { margin: 0; max-width: 720px; font-size: clamp(40px, 6vw, 78px); font-weight: 400; line-height: .95; letter-spacing: -.04em; }
 .catalog-tools { display: flex; justify-content: flex-end; margin: 0 clamp(20px, 3vw, 42px) 18px; }
 #catalog-search { width: 260px; max-width: 100%; min-height: 44px; padding: 10px 2px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: transparent; color: var(--ink); font: 16px/22px Arial, sans-serif; }
 #catalog-search::placeholder { color: var(--muted); }
 #catalog-search:focus-visible { outline: 1px solid var(--muted); outline-offset: 4px; }
 .catalog-empty { margin: 0 clamp(20px, 3vw, 42px); color: var(--muted); font: 14px/1.5 Arial, sans-serif; }
 .catalog-empty:empty, .product[hidden], .edition-divider[hidden] { display: none; }
 .edition-divider.is-first { margin-top: 0; }
 .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
 .edition-divider { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: baseline; gap: 14px; margin: 72px clamp(20px, 3vw, 42px) 8px; }
 .edition-divider:first-child { margin-top: 0; }
 .edition-divider h2 { margin: 0; font-size: clamp(28px, 3vw, 42px); font-weight: 400; line-height: 1; letter-spacing: -.025em; }
 .edition-toggle { display: flex; flex-wrap: wrap; align-items: baseline; gap: 14px; width: 100%; min-height: 44px; text-decoration: none; }
 .edition-toggle h2 { display: inline-flex; align-items: center; gap: 12px; min-height: 44px; }
 .edition-toggle:focus-visible { outline: 2px solid var(--ink); outline-offset: 5px; }
 .edition-toggle h2::before { content: "▸"; font-size: .65em; }
 .edition-toggle[aria-expanded="true"] h2::before { content: "▾"; }
 .edition-divider p { margin: 0; color: var(--muted); white-space: nowrap; text-transform: uppercase; font: 700 14px/1.2 Arial, sans-serif; letter-spacing: .06em; }
 .product { min-width: 0; padding: clamp(20px, 2vw, 28px); background: var(--paper); }
 .book-cover { position: relative; isolation: isolate; aspect-ratio: .72; display: flex; flex-direction: column; align-items: center; padding: 11% 9%; overflow: hidden; color: #f4dfb3; background: linear-gradient(135deg, var(--cover), color-mix(in srgb, var(--cover) 58%, #000)); border-radius: 2px 12px 12px 2px; box-shadow: 11px 16px 25px rgba(34,28,20,.2), inset 5px 0 8px rgba(0,0,0,.17), inset 0 0 0 1px rgba(255,255,255,.08); cursor: pointer; transition: transform 180ms ease; }
 .product:hover .book-cover { transform: translateY(-5px) rotate(-.5deg); }
 .book-cover::before { content: ""; position: absolute; z-index: -1; inset: 0; opacity: .22; background: repeating-linear-gradient(89deg, rgba(255,255,255,.18) 0 1px, transparent 1px 4px); }
 .book-cover h3 { width: 100%; margin: 5% 0 0; white-space: nowrap; text-align: center; text-transform: uppercase; font-size: clamp(19px, 2vw, 29px); font-weight: 400; letter-spacing: .11em; }
 .book-symbol { position: absolute; left: 50%; top: 50%; width: 44%; aspect-ratio: 1; transform: translate(-50%, -50%); pointer-events: none; }
 .book-symbol svg { display: block; width: 100%; height: 100%; }
 .book-cover a { text-decoration: none; }
 .book-brand { margin-top: auto; text-transform: uppercase; font: 700 7px/1 Arial, sans-serif; letter-spacing: .18em; }
 .book-preview { position: fixed; z-index: 30; inset: 0; background: rgba(23,23,20,.55); opacity: 0; visibility: hidden; transition: opacity 250ms, visibility 250ms; }
 .book-preview.is-open { opacity: 1; visibility: visible; }
 .preview-book { position: absolute; left: 50%; top: 50%; width: min(320px, 42vw, 62svh); aspect-ratio: .72; translate: 0 -50%; transform: translate(var(--x), var(--y)) scale(var(--scale)); transition: transform 450ms cubic-bezier(.2,.8,.2,1); perspective: 1400px; }
 .preview-book-video { width: min(42vw, 56svh, 560px); perspective: 1600px; }
 .book-preview.is-open .preview-book { transform: none; }
 .preview-pages { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; gap: 14px; padding: 36px 10% 22px; overflow: clip; border-radius: 2px 12px 12px 2px; background: #fffaf0; box-shadow: 11px 16px 35px rgba(0,0,0,.28), inset 7px 0 12px rgba(90,70,35,.12); }
 .preview-pages p { margin: 0; font: 400 16px/1.5 Arial, sans-serif; text-wrap: pretty; }
 .preview-pages .preview-note { color: var(--muted); font-size: 12px; line-height: 1.4; }
 .preview-pages .preview-add { font-size: 13px; }
 .preview-know { display: flex; flex-direction: column; align-items: center; gap: 8px; }
 .preview-know h4 { margin: 0; color: #7c6631; font: italic 400 14px/1 Georgia, "Times New Roman", serif; letter-spacing: .14em; }
 .preview-skills { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 0; padding: 0; list-style: none; }
 .preview-skills li { padding: 2px 10px; border: 1px solid #e4d5ad; border-radius: 999px; background: #f7eed7; color: #6b5626; font: 400 12px/1.6 Arial, sans-serif; }
 .preview-know .preview-more { margin: 0; color: #7c6631; font: italic 400 13px/1 Georgia, "Times New Roman", serif; letter-spacing: .06em; }
 .preview-close { position: absolute; z-index: 1; top: 8px; right: 8px; border: 0; background: transparent; cursor: pointer; font-size: 25px; }
 .preview-cover { position: absolute; inset: 0; height: 100%; transform-origin: left; transition: transform 650ms cubic-bezier(.2,.7,.2,1) 450ms; }
 .preview-cover h3, .preview-cover .book-signature, .preview-cover .book-symbol, .preview-cover .book-brand { transition: opacity 100ms 450ms; }
 .book-preview.is-open .preview-cover { transform: rotateY(-155deg); }
 .book-preview.is-open .preview-cover h3, .book-preview.is-open .preview-cover .book-signature, .book-preview.is-open .preview-cover .book-symbol, .book-preview.is-open .preview-cover .book-brand { opacity: 0; }
 .preview-flip { transform-style: preserve-3d; }
 .preview-flip .book-cover { width: 100%; height: 100%; }
 .preview-face { position: absolute; inset: 0; overflow: hidden; border-radius: 2px 12px 12px 2px; backface-visibility: hidden; }
 .preview-back { transform: rotateY(180deg); background: #140f0a; box-shadow: inset 0 0 26px rgba(0,0,0,.5), inset 0 0 0 1px rgba(255,255,255,.06); }
 .preview-back video { display: block; width: 100%; height: 100%; object-fit: cover; transform: scale(1.36); }
 .preview-sound { position: absolute; z-index: 2; right: 10px; bottom: 10px; display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border: 0; border-radius: 50%; background: rgba(20,15,10,.55); color: #fff; cursor: pointer; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transition: background 150ms; }
 .preview-sound:hover, .preview-sound:focus-visible { background: rgba(20,15,10,.82); }
 .preview-sound svg { width: 22px; height: 22px; }
 .preview-sound .preview-loud { display: none; }
 .preview-sound.is-on .preview-loud { display: block; }
 .preview-sound.is-on .preview-muted { display: none; }
 .product-info { display: flex; justify-content: space-between; gap: 16px; align-items: center; padding-top: 23px; }
 .product-meta p { margin: 4px 0 0; color: var(--muted); font: 12px/1.4 Arial, sans-serif; }
 .price { font-size: 19px; }
 .add-button { flex: 0 0 auto; width: 44px; height: 44px; border: 1px solid var(--ink); border-radius: 50%; background: transparent; cursor: pointer; font: 22px/1 Arial, sans-serif; transition: background 150ms, color 150ms; }
 .add-button:hover, .add-button.is-added { color: var(--paper); background: var(--ink); }
 main > .scrollSection { padding: 0 clamp(20px, 8vw, 124px); }
 main > .scrollSection h2.scrollParagraph { max-width: 720px; margin: 0 0 45px; font-size: clamp(40px, 6vw, 78px); font-weight: 400; line-height: .95; letter-spacing: -.04em; }
 main > .scrollSection:last-child { padding-bottom: 64px; }
 main > .scrollSection:last-child .scrollParagraph { border-bottom: 1px solid var(--line); }
 .scrollQuestion, .scrollQuestion + .scrollParagraph { max-width: 900px; margin-right: auto; margin-left: 0; }
 .scrollQuestion { margin-top: 0; margin-bottom: 0; padding: 24px 0 10px; border-top: 1px solid var(--line); font-size: clamp(19px, 2vw, 25px); font-weight: 400; }
 .scrollQuestion + .scrollParagraph { margin-top: 0; margin-bottom: 0; padding: 0 0 24px; color: var(--muted); font: 15px/1.6 Arial, sans-serif; }
 .scrim { position: fixed; z-index: 19; inset: 0; background: rgba(23,23,20,.36); backdrop-filter: blur(2px); }
 .cart { position: fixed; z-index: 20; left: 50%; top: 50%; width: min(520px, calc(100% - 24px)); max-height: calc(100svh - 32px); padding: 26px 32px; display: flex; flex-direction: column; background: #fffdf8; border: 1px solid #fff9; border-radius: 28px; box-shadow: 0 30px 100px #17171440; transform: translate(-50%, -50%); opacity: 0; visibility: hidden; overflow-y: auto; transition: transform 420ms cubic-bezier(.2,.8,.2,1), opacity 240ms, visibility 240ms; }
 .cart.is-open { transform: translate(-50%, -50%) scale(1); opacity: 1; visibility: visible; transition: opacity 280ms 180ms, visibility 0ms; }
 .cart-head { display: flex; justify-content: space-between; align-items: flex-start; padding-bottom: 24px; border-bottom: 1px solid var(--line); }
 .cart-head .eyebrow { margin-bottom: 7px; }
 .cart-head h2 { margin: 0; font-size: 42px; font-weight: 400; }
 .cart-close { width: 42px; height: 42px; border: 0; background: transparent; cursor: pointer; font-size: 32px; font-weight: 300; }
 .cart-lines { overflow-y: auto; min-height: 90px; flex: 1 0 auto; }
 .cart-line { display: grid; grid-template-columns: 54px minmax(0, 1fr) auto; gap: 15px; align-items: center; padding: 18px 0; border-bottom: 1px solid var(--line); }
 .line-swatch, .basket-book { position: relative; width: var(--mini-width, 54px); aspect-ratio: .72; }
 .line-swatch .book-cover, .basket-book .book-cover { position: absolute; width: 270px; padding: 29.7px 24.3px; transform: scale(var(--book-scale, .2)); transform-origin: top left; pointer-events: none; }
 .line-copy h3 { margin: 0 0 6px; font-size: 18px; font-weight: 400; }
 .line-copy p { margin: 0; color: var(--muted); font: 12px/1 Arial, sans-serif; }
 .quantity { display: flex; align-items: center; gap: 8px; }
 .quantity button { width: 27px; height: 27px; border: 1px solid var(--line); border-radius: 50%; background: transparent; cursor: pointer; }
 .quantity span { min-width: 12px; text-align: center; font: 12px/1 Arial, sans-serif; }
 .cart-empty { margin: 24px auto; text-align: center; color: var(--muted); }
 .cart-empty a { color: var(--ink); text-underline-offset: 4px; }
 .cart-footer { margin-top: auto; padding-top: 24px; border-top: 1px solid var(--line); }
 .cart-total { display: flex; justify-content: space-between; margin: 0 0 20px; font: 16px/1 Arial, sans-serif; }
 .cart-total strong { font-size: 20px; }
 .referral { margin-bottom: 20px; font: 14px/1.4 Arial, sans-serif; }
 .referral label { display: block; margin-bottom: 8px; }
 .referral input { box-sizing: border-box; width: 100%; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: transparent; color: var(--ink); font: 16px Arial, sans-serif; }
 .referral input[aria-invalid="true"] { border-color: #a33e29; }
 .referral-status { margin: 8px 0 0; color: var(--muted); }
 .referral-status:empty { display: none; }
 .paypal-button { display: block; padding: 20px; border-radius: 999px; color: white; background: #243d32; box-shadow: 0 6px 16px #243d3226; text-align: center; text-decoration: none; font: 700 14px/1 Arial, sans-serif; }
 .paypal-button:hover { background: #172e24; transform: translateY(-1px); }
 .wallet-buttons { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 12px; }
 .wallet-buttons a, .wallet-buttons button { padding: 14px 8px; border: 1px solid var(--line); border-radius: 16px; color: var(--ink); text-align: center; text-decoration: none; font: 700 14px/1.4 Arial, sans-serif; }
 .wallet-buttons a:hover, .wallet-buttons button:hover { background: #243d320a; }
 .wallet-buttons button { background: transparent; cursor: pointer; }
 #download-order-slip { display: block; width: 100%; margin-top: 18px; padding: 12px; border: 0; background: transparent; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; font: 12px Arial, sans-serif; cursor: pointer; }
 #payment-status { font: 12px/1.5 Arial, sans-serif; overflow-wrap: anywhere; }
 #payment-status:empty { display: none; }
 .wallet-buttons span { display: block; margin-top: 4px; overflow-wrap: anywhere; font-size: 11px; font-weight: 400; }
 .cart-footer small { display: block; margin-top: 12px; color: var(--muted); text-align: center; font: 10px/1.4 Arial, sans-serif; }
 .cart-footer .shipping-note { margin: -8px 0 12px; }
 .site-footer { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 360px); gap: 36px 64px; padding: 0 clamp(20px, 8vw, 124px) 32px; color: var(--muted); }
 .contact-heading { margin: 0 0 24px; color: var(--ink); font-size: clamp(38px, 4vw, 56px); font-weight: 400; line-height: 1.05; letter-spacing: -.04em; }
 .email-invite { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; margin: 0; font-size: clamp(18px, 2vw, 22px); line-height: 1.5; }
 .email-invite a { max-width: 100%; padding: 5px 0; overflow-wrap: anywhere; color: var(--ink); text-decoration-thickness: 1px; text-decoration-color: var(--line); text-underline-offset: 5px; }
 .footer-label { margin: 5px 0 16px; font: 11px/1.5 Arial, sans-serif; text-transform: uppercase; letter-spacing: .1em; }
 .footer-links { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 20px; font: 14px/1.5 Arial, sans-serif; }
 .footer-links a { display: flex; align-items: center; min-height: 44px; color: inherit; text-decoration: none; text-underline-offset: 4px; }
 .site-footer a:hover, .site-footer a:focus-visible { color: var(--ink); text-decoration: underline; }
 .footer-download { grid-column: 1 / -1; margin: 0; padding-top: 20px; border-top: 1px solid var(--line); font: 12px/1.5 Arial, sans-serif; }
 .footer-download a { display: inline-block; padding: 8px 0; color: inherit; text-decoration: none; text-underline-offset: 4px; }
 @media (max-width: 700px) { .site-footer { grid-template-columns: minmax(0, 1fr); gap: 32px; } .footer-links { max-width: 400px; } }
 noscript { display: block; padding: 12px; color: white; background: #8a2d21; text-align: center; }
 @media (max-width: 850px) { .product-grid { grid-template-columns: repeat(2, 1fr); } .section-heading { grid-template-columns: 1fr; } }
 @media (max-width: 520px) { .product-grid { grid-template-columns: 1fr; } .cart { padding: 22px; } }
 @media (max-width: 520px) { .preview-book { width: min(320px, 78vw, 62svh); left: 50%; translate: -50% -50%; } .preview-book-video { width: min(50vw, 58svh); translate: 0 -50%; } .preview-book-video .preview-pages { justify-content: flex-start; overflow-y: auto; padding: 30px 9% 14px; gap: 10px; } .preview-book-video .preview-pages p { font-size: 13px; line-height: 1.35; } .preview-book-video .preview-know h4 { font-size: 12px; } .preview-book-video .preview-skills li { padding: 1px 7px; font-size: 10px; } .preview-book-video .preview-know .preview-more { font-size: 11px; } .preview-pages { gap: 16px; } }
 @media (max-height: 440px) { .preview-book { width: min(320px, 42vw, 80svh); aspect-ratio: 1; } .preview-pages { padding: 28px 8% 14px; gap: 9px; } .preview-pages p { font-size: 13px; line-height: 1.3; } .preview-know { gap: 5px; } .preview-know h4 { font-size: 12px; } .preview-skills li { padding: 1px 8px; font-size: 10px; } }
 @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { transition-duration: .01ms !important; } }

<header class="site-header">
  <a class="brand" href="#top" aria-label="Knowbooks home"><img src="knowbooksLogo.svg" alt=""><span>KNOWBOOKS</span></a>
  <a class="faq-link" href="#faq">FAQ</a>
  <button class="cart-toggle" type="button" aria-label="Open shopping cart" aria-expanded="false" aria-controls="cart"><span class="corner-books" aria-hidden="true"><span class="basket-books"></span></span><svg viewBox="0 0 160 120" fill="none" aria-hidden="true"><path d="M13 15h19l16 69h88" stroke="#343e35" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path d="M38 34h108l-13 40H47z" fill="#c7ad7029" stroke="#343e35" stroke-width="3" stroke-linejoin="round"/><path d="M62 35l5 38m20-38v38m23-38-5 38M43 54h96" stroke="#8b805f" stroke-width="2"/><circle cx="59" cy="100" r="9" fill="#343e35"/><circle cx="126" cy="100" r="9" fill="#343e35"/><circle cx="59" cy="100" r="3" fill="#efe9da"/><circle cx="126" cy="100" r="3" fill="#efe9da"/></svg><span class="cart-count">0</span></button>
 </header>
<main id="top">
  <section class="hero">
   <h1>Knowbooks are books you <em>do</em>.</h1>
   <p class="intro">No pages. No screens. A Knowbook is an experiment kit in a book. Open one, do the experiments, and grow superpowers.</p>
   <a class="down" href="#catalog">Build your shelf <span aria-hidden="true">↓</span></a>
  </section>
  <section class="catalog-section" id="catalog" aria-labelledby="catalog-title">
   <div class="catalog-tools"><input id="catalog-search" type="search" placeholder="Search books…" aria-label="Search knowbooks" aria-controls="product-grid" title="Search book titles, signatures, or tags; separate words with spaces to match any." autocomplete="off"></div>
   <div class="edition-divider is-first" id="search-heading" hidden><h2>Search results</h2></div>
   <p class="catalog-empty" role="status" aria-live="polite"></p>
   <div class="product-grid" id="product-grid"></div>
  </section>

## Frequently asked questions
 id faq

? Who are Knowbooks for?
Knowbooks are for curious people of all ages. Younger children can explore them with an adult, while teenagers and adults can work through experiments independently.

? What comes inside each Knowbook?
Each Knowbook contains a curated collection of durable tools plus experiment cards that show you what to do to learn a superpower.

? How is the current build quality?
Our first Knowbooks are Believer editions. Each one is hand-crafted in Hawaii. They are complete and usable, but their tools, experiments, packaging, and instructions are improving as we learn from our first customers.

? Can I use a Knowbook more than once?
Yes. Knowbooks are designed to last a lifetime and can be explored repeatedly, shared, and returned to the shelf. Most objects inside are reusable tools; some contain consumable/replaceable materials.

? Where do materials inside Knowbooks come from?
Tools and other Knowbook contents come primarily from America but also Japan, China, Mexico, Canada, and other countries. Some tools and objects we make ourselves but most come from other companies.

? Where do materials used to build Knowbooks come from?
Knowbooks themselves are made from trees grown in Hawaii and America with other materials sourced primary from mainland.

? What's the support policy?
All Knowbooks come with lifetime support.

? What's the return policy?
Your satisfaction is our top priority. If we don't reach your expectations, get in touch and we'll coordinate a replacement and/or refund.

? When will my Knowbook(s) ship (Mainland customers)?
Orders begin assembly immediately. You'll receive tracking when your order is on its way. Delivery times will improve as we scale up.

? When can I pickup my Knowbook(s) (Hawaii customers)?
Hawaii customers can have their Knowbooks delivered free of charge, or come pick them up.

? What are 808 Knowbooks?
All Knowbooks go through many prototype editions. Some of these prototypes we refurbish and sell at discounted prices at local markets in Hawaii. Though these prototype Knowbooks are quirky, like everything we make, we back 808 Knowbooks with lifetime support.

section
 addClass offering
 id signature
 aria-labelledby signature-title
 div
  id signature-element-1
  addClass section-heading
  a Signature Knowbooks
   href #signature
   id signature-element-2
   addClass eyebrow
  h2 Your expertise.<br>Your signature.<br>Your Knowbook.
   id signature-title
 div
  id signature-element-3
  addClass offering-body
  p Create a Knowbook with us. Put your life's work in people's hands.
   id signature-element-4
   addClass offering-lead
  p We partner with renowned experts to jointly create Signature Knowbooks. Our world-class team designs the experiments with your feedback, turning your expertise into hands-on discoveries people can explore for themselves.
   id signature-element-5
   addClass offering-copy
  dl
   id signature-element-6
   addClass offering-terms
   div
    id signature-element-7
    dt Create together.
     id signature-element-8
    dd Bring your knowledge and community. We handle the grunt work from manufacturing to fulfillment.
     id signature-element-9
   div
    id signature-element-10
    dt Share the success.
     id signature-element-11
    dd Order copies at our cost + 30%; sell, give them away, or have us fulfill.
     id signature-element-12
  a Create your Signature Knowbook <span aria-hidden="true">↗</span>
   id signature-element-13
   addClass down
   href #designer

section
 addClass offering
 id startups
 aria-labelledby startups-title
 div
  id startups-element-1
  addClass section-heading
  a Knowbooks for startups
   href #startups
   id startups-element-2
   addClass eyebrow
  h2 Put your pitch<br>in their hands.
   id startups-title
 div
  id startups-element-3
  addClass offering-body
  p A one-of-a-kind tactile pitch kit to help your startup raise capital.
   id startups-element-4
   addClass offering-lead
  p We develop custom Knowbooks for startups raising funds. Turn your big idea into hands-on experiments that help investors explore what you're building and understand why it matters.
   id startups-element-5
   addClass offering-copy
  dl
   id startups-element-6
   addClass offering-terms
   div
    id startups-element-7
    dt Design once.
     id startups-element-8
    dd Pay a one-time design fee to develop your custom Knowbook.
     id startups-element-9
   div
    id startups-element-10
    dt Build on demand.
     id startups-element-11
    dd Order more, any quantity, whenever you need it.
     id startups-element-12
  a Let's build your Knowbook <span aria-hidden="true">↗</span>
   id startups-element-13
   addClass down
   href #designer

section
 addClass offering
 id training
 aria-labelledby training-title
 div
  id training-element-1
  addClass section-heading
  a Knowbooks for corporate training
   href #training
   id training-element-2
   addClass eyebrow
  h2 Train your team.<br>Faster. Better. Deeper.
   id training-title
 div
  id training-element-3
  addClass offering-body
  p Turn hands-on practice into deeper understanding and skills your team can put to work.
   id training-element-4
   addClass offering-lead
  p We create custom Knowbooks around the skills your team needs to master. An alternative to training videos and manuals, each book gives your team experiments they can do with their hands: practice the task, see how it works, and understand why. Build your training around doing, so your team can get up to speed and go deeper.
   id training-element-5
   addClass offering-copy
  a Build your team's training Knowbook <span aria-hidden="true">↗</span>
   id training-element-6
   addClass down
   href #designer

section
 addClass offering
 id partners
 aria-labelledby partners-title
 div
  id partners-element-1
  addClass section-heading
  a Knowbooks partners
   href #partners
   id partners-element-2
   addClass eyebrow
  h2 Made locally.<br>Copied worldwide.
   id partners-title
 div
  id partners-element-3
  addClass offering-body
  p Our focus is Hawaii. Our partners build around the world.
   id partners-element-4
   addClass offering-lead
  p We partner with exceptional teams all over the world to bring more Knowbooks to more people faster. Interested in building Knowbooks for your community? Talk to us about becoming a Knowbooks partner.
   id partners-element-5
   addClass offering-copy
  a Become a Knowbooks partner <span aria-hidden="true">↗</span>
   id partners-element-6
   addClass down
   href #partner-dialog

</main>

<footer class="site-footer">
  <div>
  <h2 class="contact-heading">Contact us</h2>
  <p class="email-invite"><a href="mailto:founders@knowbooks.com">founders@knowbooks.com</a><a href="tel:+18083682541">1-808-368-2541</a></p>
  </div>
  <div>
  <p class="footer-label">&nbsp;</p>
  <nav class="footer-links" aria-label="Site and social links">
  <a href="https://videos.knowbooks.com">Videos</a>
  <a href="https://x.com/breckyunits">X</a>
  <a href="https://github.com/breck7/knowbooks.com">GitHub</a>
  <a href="https://www.linkedin.com/">LinkedIn</a>
  <a href="https://www.youtube.com/watch?v=dHKnfVTSREo&list=PLKKmj7IZB5DE">YouTube</a>
  <a href="https://www.instagram.com/breck7/">Instagram</a>
  <a href="https://www.tiktok.com/@knowbooks.com">TikTok</a>
  <a href="https://www.facebook.com/breck7/">Facebook</a>
  <a href="https://bsky.app/profile/breck7.bsky.social">Bluesky</a>
  <a href="https://x.com/i/lists/2099164377492369481">ScienceFeed</a>
  <a href="s/index.html">Free software</a>
  <a href="https://gab.com/breck7">Gab</a>
  <a href="https://truthsocial.com/@brecky">Truth Social</a>
  </nav>
  </div>
  <p class="footer-download"><a href="knowbooks.zip" download>Download this website</a></p>
 </footer>

<div class="scrim" hidden></div>
<div class="book-preview" role="dialog" aria-modal="true" aria-label="Inside this Knowbook" hidden></div>
aside
 addClass cart
 role dialog
 aria-modal true
 inert
 id cart
 aria-labelledby cart-title
 aria-hidden true
 div
  id cart-element-1
  addClass cart-head
  div
   id cart-element-2
   p
    id cart-element-3
    addClass eyebrow
   h2 Your cart
    id cart-title
  button ×
   id cart-element-4
   addClass cart-close
   type button
   aria-label Close cart
 div
  id cart-element-5
  addClass cart-art
  aria-hidden true
  div
   id cart-element-6
   addClass basket-books
  inlineSvg
   <svg viewBox="0 0 160 120" fill="none" aria-hidden="true"><path d="M13 15h19l16 69h88" stroke="#343e35" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path d="M38 34h108l-13 40H47z" fill="#c7ad7029" stroke="#343e35" stroke-width="3" stroke-linejoin="round"/><path d="M62 35l5 38m20-38v38m23-38-5 38M43 54h96" stroke="#8b805f" stroke-width="2"/><circle cx="59" cy="100" r="9" fill="#343e35"/><circle cx="126" cy="100" r="9" fill="#343e35"/><circle cx="59" cy="100" r="3" fill="#efe9da"/><circle cx="126" cy="100" r="3" fill="#efe9da"/></svg>
 div
  id cart-element-7
  addClass cart-lines
 div
  id cart-element-8
  addClass cart-empty
  p Your cart is empty.
   id cart-element-9
  a Find a Knowbook
   id cart-element-10
   href #catalog
 div
  id cart-element-11
  addClass cart-footer
  htmlHidden
  div
   id cart-element-12
   addClass referral
   input
    id referral-code
    type text
    placeholder Referral code (optional)
    autocomplete off
    autocapitalize none
    spellcheck false
    aria-describedby referral-status
   p
    addClass referral-status
    id referral-status
    role status
  p
   id cart-element-13
   addClass cart-total
   span Total
    id cart-element-14
   strong $0
    id cart-element-15
  small Includes shipping and tax.
   id cart-element-16
   addClass shipping-note
  a Checkout with PayPal &nbsp; ↗
   id cart-element-17
   addClass paypal-button
   data-payment PayPal
   target _blank
   href https://www.paypal.com/cgi-bin/webscr
   rel noopener
  div
   addClass wallet-buttons
   id payment-options
   a Cash App ↗
    id pay-cash-app
    data-payment Cash App
    href https://cash.app/$knowbooksdotcom
    target _blank
    rel noopener
    span $knowbooksdotcom
     id cash-app-account
   a Venmo ↗
    id pay-venmo
    data-payment Venmo
    href https://venmo.com/u/knowbooks
    target _blank
    rel noopener
    span @knowbooks
     id venmo-account
   button NEAR
    id pay-near
    type button
    data-payment NEAR
    data-address knowbooks.near
    span knowbooks.near
     id near-caption
   button Bitcoin
    id pay-bitcoin
    type button
    data-payment BTC
    data-address 3G9q4tRjkTSP8PP4eFVZuNLtXxKuERJcJr
    span Copy address
     id bitcoin-caption
   button Ethereum
    id pay-ethereum
    type button
    data-payment ETH
    data-address 0xaEe37CE75AcC6341abfa1c73c6Bb1B73963a6ccD
    span Copy address
     id ethereum-caption
   button USDC · Base
    id pay-usdc
    type button
    data-payment USDC (Base)
    data-address 0x3d3e69E8f208A782E09f018643fE9796e918e45e
    span Copy address
     id usdc-caption
   button Cash
    id pay-cash
    type button
    data-payment Cash
    span Accepted in Hawaii
     id cash-caption
   button Check
    id pay-check
    type button
    data-payment Check
    span Accepted in Hawaii
     id check-caption
  p
   id payment-status
   role status
 button Download Receipt
  id download-order-slip
  type button
<dialog id="designer" aria-labelledby="designer-heading">
 <form id="design-form">
 <div class="designer-head"><h2 id="designer-heading">Your Signature Knowbook</h2><button type="button" id="design-close" aria-label="Close designer">×</button></div>
 <div class="design-spread">
 <div><div class="design-cover">
 <h3 id="design-title" contenteditable="plaintext-only" role="textbox" aria-label="Knowbook title" aria-multiline="false" spellcheck="false">Your Knowbook</h3>
 <span class="design-signature-wrap"><input id="design-signature" aria-label="Expert name" placeholder="Your name (optional)" maxlength="60"><span id="design-signature-art" aria-hidden="true"></span></span>
 <svg id="design-art" viewBox="0 0 256 256" tabindex="0" aria-label="Draw your cover with the marker"><g id="design-portrait"></g><g class="strokes"></g></svg>
 <span>KNOWBOOKS</span></div>
 <div id="design-colors" role="group" aria-label="Cover color"></div>
 <div class="design-tools"><button type="button" id="design-marker" aria-pressed="false" aria-label="Flip marker to eraser"><svg viewBox="0 0 100 24" width="100" height="24" aria-hidden="true"><path d="M5 12 19 5v14z" fill="currentColor"/><rect x="19" y="4" width="60" height="16" rx="3" fill="currentColor"/><rect x="80" y="4" width="15" height="16" rx="3" fill="#d9bc78"/></svg><span>Flip to erase</span></button><button type="button" id="design-undo">Undo</button><label class="design-upload">Upload<input id="portrait-file" type="file" aria-label="Upload cover image" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml,.svg"></label><button type="button" id="remove-image" hidden>Remove image</button></div>
 </div>
 <div class="design-inside"><div id="design-experiments"><h3><label for="design-superpowers">What superpowers will your Knowbook teach?</label></h3><input id="design-superpowers" placeholder="write 5 verbs...." maxlength="124" aria-describedby="design-superpowers-help"><small id="design-superpowers-help">We’ll work with you to design 5 hands-on experiments using real objects.</small></div><p class="design-price">$10,000 <small>design fee</small></p><p>50% now. One prototype shipped within 30 days of deposit. Balance upon delivery.</p><p>More copies: you set price and keep 80% of net revenue. We handle manufacturing, distribution, and delivery.</p><button id="design-add" type="submit">Add $5,000 deposit to cart</button><p id="design-status" role="status"></p></div>
 </div><details id="design-source"><summary>See/Edit code</summary><textarea id="design-code" aria-label="Knowbook Scroll code" aria-describedby="design-code-status" spellcheck="false"></textarea><small id="design-code-status" role="status"></small><label for="design-link">Design link</label><input id="design-link" type="text" readonly spellcheck="false"></details></form>
</dialog>
partners.scroll

<noscript>This shop needs JavaScript to manage its cart.</noscript>

gitHash.js
signature.js
covers.js
catalog.js
tags.js
s/qrcode/qrcode.js
s/checko/checko.js

script
 (() => {
  "use strict";
  const coverPalette = {
   navy: "#294575",
   ocean: "#05638E",
   teal: "#0E91A5",
   forest: "#3D6640",
   purple: "#7D589E",
   yellow: "#F2C75D",
   orange: "#E88F47",
   coral: "#EC704B",
   brick: "#B84A40",
   burgundy: "#8D3340",
   brown: "#65453B"
  };
  const coverColors = ["yellow", "navy", "coral", "forest", "orange", "purple", "brick", "teal", "burgundy", "ocean", "brown"].map(color => coverPalette[color]);
  let nextCoverColor = 0;
  const productCatalog = (Array.isArray(catalog) ? catalog : []).map(product => {
   const color = product.color?.trim().toLowerCase();
   if (color && !Object.hasOwn(coverPalette, color)) throw new Error(`Unknown cover color "${product.color}" for ${product.id}`);
   return { ...product, coverColor: color ? coverPalette[color] : coverColors[nextCoverColor++ % coverColors.length] };
  });
  const products = new Map(productCatalog.map(product => [product.id, product]));
  const productTags = new Map(productCatalog.map(product => [product.id, new Set((product.tags || "").split(/\s+/).filter(Boolean))]));
  const tagMembers = tags.map(tag => new Set(productCatalog.filter(product => productTags.get(product.id).has(tag.id)).map(product => product.id)));
  const tagVectors = new Map(productCatalog.map(product => [product.id, tagMembers.map(members => Number(members.has(product.id)))]));
  const searchableTitles = new Map(productCatalog.map(product => [product.id, [product.id, product.title, product.signature || "", ...tags.filter((tag, index) => tagMembers[index].has(product.id)).map(tag => `${tag.id} ${tag.keywords || ""}`)].join(" ").toLowerCase()]));
  const searchableDetails = new Map(productCatalog.map(product => [product.id, [product.skills || "", product.tools || ""].join(" ").toLowerCase()]));
  const vectorDistance = (a, b) => {
   const left = tagVectors.get(a), right = tagVectors.get(b);
   return 1 - left.reduce((sum, bit, i) => sum + (bit & right[i]), 0) / (left.reduce((sum, bit, i) => sum + (bit | right[i]), 0) || 1);
  };
  const paypalEmail = "breck7@gmail.com";
  const currency = "USD";
  const money = new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 });
  const checkoutMoney = new Intl.NumberFormat("en-US", { style: "currency", currency });
  const referralCodes = new Set("john kendric liz daylight kemi mikayla joe blake daniel francisco butler hassam ash goodman frank ari leah mystery nico cora yc".split(" "));
  const shippingByShelf = {
   believer: "Ship within 30 days",
   frontier: "Ship within 6 months"
  };
  const editionNameByShelf = {
   believer: "Believer",
   frontier: "Frontier"
  };
  const shelfOrder = ["believer", "frontier", "special"];
  const collapsedEditions = new Set(["frontier", "special"]);
  const cart = new Map();
  const grid = document.querySelector("#product-grid");
  const preview = document.querySelector(".book-preview");
  const drawer = document.querySelector("#cart");
  const scrim = document.querySelector(".scrim");
  const toggle = document.querySelector(".cart-toggle");
  const count = document.querySelector(".cart-count");
  const lines = document.querySelector(".cart-lines");
  const empty = document.querySelector(".cart-empty");
  const footer = document.querySelector(".cart-footer");
  const total = document.querySelector(".cart-total strong");
  const checkout = document.querySelector(".paypal-button");
  const referralInput = document.querySelector("#referral-code");
  const referralStatus = document.querySelector("#referral-status");
  const cartPricing = () => {
   const enteredCode = referralInput.value.trim().toLowerCase();
   const referralCode = referralCodes.has(enteredCode) ? enteredCode : "";
   const subtotalCents = [...cart].reduce((sum, [id, quantity]) => sum + Math.round(products.get(id).priceUsd * 100) * quantity, 0);
   const discountAmount = referralCode === "yc" ? -200 : 10
   const discountCents = referralCode ? Math.round(subtotalCents * (discountAmount/100)) : 0;
   return { enteredCode, discountAmount, referralCode, discountCents, amount: (subtotalCents - discountCents) / 100 };
  };
  const escapeHtml = value => String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
  // A book's signature is seeded by its title plus its author, so each book stays
  // stable while the same author's hand varies slightly from book to book.
  const signatureFor = (title, author) => author ? getSignature(author, `${title || ""}\u0000${author}`) : "";
  const shelvesOf = product => (product.shelf || "").trim().split(/\s+/).filter(Boolean);
  const primaryShelf = product => shelvesOf(product)[0];
  const cardId = (product, shelf) => shelf === primaryShelf(product) ? product.id : `${product.id}--${shelf}`;
  const editionDetails = shelf => {
   if (shelf === "special") return { name: "Special items", shipping: "" };
   const name = editionNameByShelf[shelf];
   const shipping = shippingByShelf[shelf];
   if (!name || !shipping) throw new Error(`Unknown product shelf: ${shelf}`);
   return { name, shipping };
  };
  const pickupNote = product => product.fulfillment === "pickup" ? "In-person pickup only — will not be shipped." : "";
  const skillsOf = product => (product.skills || product.design?.superpowers || "").trim().split(/\s+/).filter(Boolean);
  const toolsOf = product => (product.tools || "").trim().split(/\s+/).filter(Boolean);
  const chipSection = (label, items) => items.length ? `<div class="preview-know"><h4>${escapeHtml(label)}</h4><ul class="preview-skills">${items.slice(0, 5).map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ul><p class="preview-more">and more!</p></div>` : "";
  const cartDeliveryNote = () => [...cart.keys()].some(id => pickupNote(products.get(id))) ? "Includes tax and shipping for shippable items. Pickup-only items must be collected in person." : "Includes shipping and tax.";
  const bookCover = product => `<div class="book-cover" style="--cover:${escapeHtml(product.coverColor)}">
     <h3><a href="?knowbookId=${escapeHtml(product.id)}" onclick="return false">${escapeHtml(product.design?.title || product.title)}</a></h3>${(product.design?.signature || product.signature) ? `<span class="book-signature">${signatureFor(product.design?.title || product.title, product.design?.signature || product.signature)}</span>` : ""}<span class="book-symbol" aria-hidden="true">${product.cover || ""}</span><span class="book-brand">Knowbooks</span>
    </div>`;
  const productCard = (product, shelf) => {
   const { name, shipping } = editionDetails(shelf);
   return `<article class="product" id="${escapeHtml(cardId(product, shelf))}" data-product-id="${escapeHtml(product.id)}" data-shelf="${escapeHtml(shelf)}">
    ${bookCover(product)}
    <div class="product-info"><div class="product-meta"><span class="price">${money.format(product.priceUsd)}</span><p>${escapeHtml(product.description || (shelf === "special" ? "" : "Experiment kit in a book."))}${shelf === "special" ? (pickupNote(product) ? `<br><i>${escapeHtml(pickupNote(product))}</i>` : "") : `<br><i>${escapeHtml(name)} edition<br>${escapeHtml(pickupNote(product) || shipping)}</i>`}</p></div><button class="add-button" type="button" data-add="${escapeHtml(product.id)}" aria-label="Add ${escapeHtml(product.title)} to cart">+</button></div>
   </article>`;
  };
  const editionDivider = shelf => {
   const { name, shipping } = editionDetails(shelf);
   const heading = shelf === "special" ? name : `${name} editions`;
   const sectionId = shelf === "special" ? "special-items" : `${shelf}-editions`;
   const controls = productCatalog.filter(product => shelvesOf(product).includes(shelf)).map(product => escapeHtml(cardId(product, shelf))).join(" ");
   return `<div class="edition-divider" id="${sectionId}" data-shelf="${escapeHtml(shelf)}"><a class="edition-toggle" href="#${sectionId}" role="button" aria-expanded="${!collapsedEditions.has(shelf)}" aria-controls="${controls}"><h2>${escapeHtml(heading)}</h2>${shipping ? `<p>${escapeHtml(shipping)}</p>` : ""}</a></div>`;
  };
  const filterCatalog = () => {
   const query = document.querySelector("#catalog-search").value.trim();
   const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
   const rank = product => [product.id, product.title.toLowerCase()].includes(query.toLowerCase()) ? 0 : terms.some(term => [product.title, product.signature || ""].some(value => value.toLowerCase().includes(term))) ? 1 : 2;
   const direct = !query ? [] : productCatalog.filter(product => terms.some(term => searchableTitles.get(product.id).includes(term))).sort((a, b) => rank(a) - rank(b));
   const directIds = new Set(direct.map(product => product.id));
   const seeds = (direct.some(product => rank(product) === 0) ? direct.filter(product => rank(product) === 0) : direct).map(product => product.id);
   // At most six neighbors with >=75% overlap in Yes answers; ties keep catalog order.
   const neighbors = seeds.length ? productCatalog.filter(product => !directIds.has(product.id)).map(product => ({
    id: product.id,
    distance: Math.min(...seeds.map(id => vectorDistance(product.id, id)))
   })).filter(product => product.distance <= 0.25).sort((a, b) => a.distance - b.distance).slice(0, 6) : [];
   const hits = [...direct, ...neighbors], hitIds = new Set(hits.map(product => product.id));
   // After the title/tag results, append products whose skills or tools lines match the query.
   if (query) productCatalog.forEach(product => { if (!hitIds.has(product.id) && terms.some(term => searchableDetails.get(product.id).includes(term))) { hits.push(product); hitIds.add(product.id); } });
   if (query) hits.forEach(product => grid.appendChild(document.getElementById(product.id)));
   else catalogNodes.forEach(node => grid.appendChild(node));
   const url = new URL(location.href);
   query ? url.searchParams.set("q", query) : url.searchParams.delete("q");
   if (url.href !== location.href) history.replaceState(null, "", url);
   document.querySelector("#search-heading").hidden = !query;
   const visibleShelves = new Set();
   grid.querySelectorAll(".product").forEach(card => {
    const shelf = card.dataset.shelf;
    const matches = !query || (hitIds.has(card.dataset.productId) && card.id === card.dataset.productId);
    card.hidden = !matches || (!query && collapsedEditions.has(shelf));
    if (matches) visibleShelves.add(shelf);
   });
   const firstShelf = shelfOrder.find(shelf => visibleShelves.has(shelf));
   grid.querySelectorAll(".edition-divider").forEach(divider => {
    divider.querySelector(".edition-toggle").setAttribute("aria-expanded", String(!collapsedEditions.has(divider.dataset.shelf)));
    divider.hidden = Boolean(query) || !visibleShelves.has(divider.dataset.shelf);
    divider.classList.toggle("is-first", divider.dataset.shelf === firstShelf);
   });
   document.querySelector(".catalog-empty").textContent = visibleShelves.size ? "" : "No knowbooks match this search.";
   fitBookTitles();
  };
  let titleFitFrame;
  const fitBookTitles = () => {
   cancelAnimationFrame(titleFitFrame);
   titleFitFrame = requestAnimationFrame(() => {
    document.querySelectorAll(".product:not([hidden]) .book-cover h3, .line-swatch .book-cover h3, .basket-book .book-cover h3").forEach(title => {
     title.style.removeProperty("font-size");
     if (title.scrollWidth > title.clientWidth) {
      const baseSize = Number.parseFloat(getComputedStyle(title).fontSize);
      title.style.fontSize = `${baseSize * title.clientWidth / title.scrollWidth}px`;
     }
    });
   });
  };
  const paypalUrl = ({ referralCode, discountCents }) => {
   const url = new URL("https://www.paypal.com/cgi-bin/webscr");
   url.searchParams.set("cmd", "_cart");
   url.searchParams.set("upload", "1");
   url.searchParams.set("business", paypalEmail);
   url.searchParams.set("currency_code", currency);
   if (referralCode) {
    url.searchParams.set("custom", `referral_code=${referralCode}`);
    if (discountCents > 0) url.searchParams.set("discount_amount_cart", (discountCents / 100).toFixed(2));
   }
   [...cart].forEach(([id, quantity], index) => {
    const product = products.get(id);
    const number = index + 1;
    url.searchParams.set(`item_name_${number}`, `${product.title}${product.shelf === "special" && product.id !== "knowbook" ? "" : " Knowbook"}${product.id === "knowbook" ? " + initial partner conversation" : ""}${pickupNote(product) ? " — in-person pickup only; no shipping" : ""}${referralCode ? ` (Referral: ${referralCode})` : ""}`);
    url.searchParams.set(`item_number_${number}`, product.id);
    url.searchParams.set(`amount_${number}`, Number(product.priceUsd).toFixed(2));
    url.searchParams.set(`quantity_${number}`, String(quantity));
   });
   return url.href;
  };
  let designToScroll, restoreDesignFromData, productFromDesign;
  const base64Url = text => btoa(unescape(encodeURIComponent(text))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
  const fromBase64Url = text => decodeURIComponent(escape(atob(text.replace(/-/g, '+').replace(/_/g, '/'))));
  const knowbooksReceiptLogo = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="#d9bc78"><path fill-rule="evenodd" d="M128 14C68.45 14 20 62.45 20 122c0 18.12 4.5 35.2 12.42 50.2l12.37-7.15A93.28 93.28 0 0 1 34.3 122c0-51.67 42.03-93.7 93.7-93.7s93.7 42.03 93.7 93.7a93.28 93.28 0 0 1-10.49 43.05l12.37 7.15A107.42 107.42 0 0 0 236 122c0-59.55-48.45-108-108-108Z"/><path d="M29.31 156.42c-4.34-7.23-3.66-13.77.07-15.84 3.38-1.87 7.33.31 10.53 4.4l22.82 25.16c1.12 1.24 2.85-.31 1.88-1.66l-19.1-26.67c-3.67-5.13-3.33-10.19.2-12.41 3.77-2.38 8.54.65 12.52 5.26l18.1 20.97c3.7 4.29 8.36 7.65 12.05 12.6 5.96 7.99 6.08 19.48 6.08 31.36v25.99c-24.1-2.86-43.02-12.51-53.85-26.48-7.38-9.52-8.76-23.53-11.3-42.68Z"/><path d="M226.69 156.42c4.34-7.23 3.66-13.77-.07-15.84-3.38-1.87-7.33.31-10.53 4.4l-22.82 25.16c-1.12 1.24-2.85-.31-1.88-1.66l19.1-26.67c3.67-5.13 3.33-10.19-.2-12.41-3.77-2.38-8.54.65-12.52 5.26l-18.1 20.97c-3.7 4.29-8.36 7.65-12.05 12.6-5.96 7.99-6.08 19.48-6.08 31.36v25.99c24.1-2.86 43.02-12.51 53.85-26.48 7.38-9.52 8.76-23.53 11.3-42.68Z"/></g></svg>')}`;
  const orderLink = () => {
   const items = [...cart].map(([id, quantity]) => [id, quantity, products.get(id).design || 0]);
   return `${location.origin}${location.pathname}#order=${KNOWBOOKS_GIT_HASH}${base64Url(JSON.stringify(items))}`;
  };
  const orderReceipt = async () => {
   if (!cart.size) return;
   const { amount, discountCents, referralCode } = cartPricing();
   const rows = [...cart].map(([id, quantity]) => {
    const product = products.get(id);
    return { title: product.design?.title || product.title, quantity, total: product.priceUsd * quantity };
   });
   const W = 640, scale = 2, pad = 48, rowH = 34, qrSize = 200;
   let qr = null;
   try { qr = qrcode(0, 'L'); qr.addData(orderLink()); qr.make(); } catch { qr = null; }
   const qrModules = qr ? qr.getModuleCount() : 0, qrCell = qr ? Math.max(1, Math.floor(qrSize / qrModules)) : 0, qrPx = qrCell * qrModules, qrPad = qr ? 4 * qrCell : 0;
   const totalsH = 6 + 26 + (discountCents ? 24 : 0) + 46;
   const H = pad + 190 + rows.length * rowH + totalsH + (qr ? qrPx + qrPad + 46 : 40) + pad;
   const canvas = document.createElement('canvas');
   canvas.width = W * scale; canvas.height = H * scale;
   const ctx = canvas.getContext('2d'); ctx.scale(scale, scale);
   const ink = '#20211e', gold = '#d9bc78', muted = '#7d7a70';
   ctx.fillStyle = '#fffdf8'; ctx.fillRect(0, 0, W, H);
   ctx.strokeStyle = gold; ctx.lineWidth = 2; ctx.strokeRect(12, 12, W - 24, H - 24);
   const text = (value, x, y, font, color = ink, align = 'left') => { ctx.font = font; ctx.fillStyle = color; ctx.textAlign = align; ctx.fillText(value, x, y); };
   const clip = (value, max) => { let s = String(value); while (s.length > 1 && ctx.measureText(s + '…').width > max) s = s.slice(0, -1); return s + (s.length < String(value).length ? '…' : ''); };
   const logo = new Image();
   await new Promise(resolve => { logo.onload = resolve; logo.onerror = resolve; logo.src = knowbooksReceiptLogo; });
   if (logo.naturalWidth) ctx.drawImage(logo, W / 2 - 28, pad, 56, 56);
   text('KNOWBOOKS', W / 2, pad + 86, '600 19px Georgia, serif', ink, 'center');
   text('ORDER RECEIPT', W / 2, pad + 106, '12px Arial, sans-serif', muted, 'center');
   text(new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeStyle: 'short' }).format(new Date()), W / 2, pad + 126, '12px Arial, sans-serif', muted, 'center');
   let y = pad + 160;
   ctx.strokeStyle = '#e7dfd0'; ctx.lineWidth = 1;
   ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke();
   y += 30;
   rows.forEach(row => {
    const font = '15px Georgia, serif';
    ctx.font = font;
    text(clip(row.title, W - pad * 2 - 150), pad, y, font);
    text(`${checkoutMoney.format(row.total)}${row.quantity > 1 ? `  ×${row.quantity}` : ''}`, W - pad, y, '14px Arial, sans-serif', ink, 'right');
    y += rowH;
   });
   y += 6;
   ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke();
   y += 26;
   text('Subtotal', pad, y, '13px Arial, sans-serif', muted);
   text(checkoutMoney.format(amount + discountCents / 100), W - pad, y, '13px Arial, sans-serif', ink, 'right');
   y += 24;
   if (discountCents) {
    text(`Referral ${referralCode}`, pad, y, '13px Arial, sans-serif', muted);
    text(`\u2212${checkoutMoney.format(Math.abs(discountCents) / 100)}`, W - pad, y, '13px Arial, sans-serif', ink, 'right');
    y += 24;
   }
   text('Total', pad, y, '600 17px Arial, sans-serif');
   text(checkoutMoney.format(amount), W - pad, y, '600 17px Arial, sans-serif', ink, 'right');
   y += 46;
   if (qr) {
    const x0 = (W - qrPx) / 2;
    ctx.fillStyle = '#ffffff'; ctx.fillRect(x0 - qrPad, y - qrPad, qrPx + 2 * qrPad, qrPx + 2 * qrPad);
    ctx.fillStyle = ink;
    for (let r = 0; r < qrModules; r++) for (let c = 0; c < qrModules; c++) if (qr.isDark(r, c)) ctx.fillRect(x0 + c * qrCell, y + r * qrCell, qrCell, qrCell);
    y += qrPx + qrPad + 24;
    text('Scan to reorder or clone this order', W / 2, y, '13px Arial, sans-serif', ink, 'center');
    y += 22;
    text('knowbooks.com', W / 2, y, '11px Arial, sans-serif', muted, 'center');
   } else {
    text('This order is too large to fit in a QR code.', W / 2, y + 20, '13px Arial, sans-serif', muted, 'center');
   }
   await new Promise(resolve => canvas.toBlob(blob => {
    if (blob) {
     const url = URL.createObjectURL(blob);
     const link = Object.assign(document.createElement('a'), { href: url, download: 'knowbooks-receipt.png' });
     document.body.append(link); link.click(); link.remove();
     setTimeout(() => URL.revokeObjectURL(url), 10000);
    }
    resolve();
   }, 'image/png'));
  };
  document.querySelector('#download-order-slip').addEventListener('click', () => orderReceipt());
  drawer.addEventListener('click', event => {
   const button = event.target.closest('[data-payment]'); if (!button) return;
   if (!cart.size) { event.preventDefault(); return; }
   const address = button.dataset.address;
   if (address) {
    const status = document.querySelector('#payment-status');
    const fallback = () => { status.textContent = `Copy ${button.dataset.payment} address: ${address}`; };
    if (navigator.clipboard?.writeText) navigator.clipboard.writeText(address).then(() => { status.textContent = `${button.dataset.payment} address copied.`; }, fallback);
    else fallback();
   }
  });
  const save = () => {
   try {
    localStorage.setItem("knowbooks-cart", JSON.stringify([...cart].map(([id, quantity]) => [id, quantity, products.get(id).design || null])));
    return true;
   } catch { return false; }
  };
  const refresh = () => {
   const quantity = [...cart.values()].reduce((sum, value) => sum + value, 0);
   const pricing = cartPricing();
   const { enteredCode, referralCode, discountCents, amount, discountAmount } = pricing;
   count.textContent = quantity;
   toggle.setAttribute("aria-label", `Open shopping cart, ${quantity} ${quantity === 1 ? "book" : "books"}`);
   const bookPile = [...cart].flatMap(([id, quantity]) => Array.from({ length: Math.min(quantity, 5) }, () => products.get(id))).slice(0, 7).map((product, i) => `<span class="basket-book" style="--tilt:${(i % 3 - 1) * 12}deg" aria-hidden="true" inert>${bookCover(product)}</span>`).join("");
   document.querySelectorAll(".cart-toggle .basket-books, #cart .basket-books").forEach(pile => { pile.innerHTML = bookPile; pile.style.setProperty("--pile-overlap", `${Math.min(30, 62 / Math.max(1, pile.children.length - 1)) - 48}px`); });
   grid.querySelectorAll("[data-add]").forEach(button => {
    const added = cart.has(button.dataset.add);
    button.classList.toggle("is-added", added);
    button.textContent = added ? "✓" : "+";
   });
   lines.innerHTML = [...cart].map(([id, value]) => {
    const product = products.get(id);
    return `<div class="cart-line"><div class="line-swatch" aria-hidden="true" inert>${bookCover(product)}</div><div class="line-copy"><h3>${escapeHtml(product.title)}</h3><p>${money.format(product.priceUsd)} ${product.design ? "deposit · 50% of $10,000" : "each"}</p>${product.description ? `<p>${escapeHtml(product.description)}</p>` : ""}${pickupNote(product) ? `<p><strong>${escapeHtml(pickupNote(product))}</strong></p>` : ""}${product.design ? `<div class="design-actions"><button type="button" data-edit-design="${escapeHtml(id)}">Edit</button></div>` : ""}</div><div class="quantity"><button type="button" data-less="${escapeHtml(id)}" aria-label="Remove one ${escapeHtml(product.title)}">−</button><span>${value}</span><button type="button" data-more="${escapeHtml(id)}" aria-label="Add one ${escapeHtml(product.title)}">+</button></div></div>`;
   }).join("");
   fitBookTitles();
   empty.hidden = quantity > 0;
   footer.hidden = quantity === 0;
   total.textContent = checkoutMoney.format(amount);
   document.querySelector("#cart-element-16").textContent = cartDeliveryNote();
   referralInput.setAttribute("aria-invalid", String(Boolean(enteredCode && !referralCode)));
   referralStatus.textContent = referralCode ? `${discountAmount}% code applied — you save ${checkoutMoney.format(discountCents / 100)}.` : enteredCode ? "That referral code isn’t valid." : "";
   checkout.href = quantity ? paypalUrl(pricing) : "https://www.paypal.com/cgi-bin/webscr";
   return save();
  };
  const reducedMotion = () => matchMedia("(prefers-reduced-motion: reduce)").matches;
  let returnFocus, previewFocus, adding = false;
  const backgroundInert = value => { document.querySelectorAll(".site-header, main, .site-footer").forEach(element => { element.inert = value; }); };
  const cartArt = document.querySelector(".cart-art");
  let cartFlight;
  const stopCartFlight = () => {
   if (!cartFlight) return null;
   const rect = cartFlight.getBoundingClientRect();
   cartFlight.getAnimations().forEach(animation => animation.cancel());
   cartFlight.remove();
   cartFlight = null;
   return rect;
  };
  const tableCartRect = () => {
   const rect = drawer.getBoundingClientRect();
   return { left: rect.left + rect.width / 2 - 100, top: rect.top + rect.height / 2 - 74, width: 200, height: 148 };
  };
  const gatherBooks = target => {
   if (reducedMotion()) return;
   const bounds = drawer.getBoundingClientRect();
   lines.querySelectorAll(".line-swatch").forEach(swatch => {
    const rect = swatch.getBoundingClientRect();
    if (rect.bottom < bounds.top || rect.top > bounds.bottom) return;
    const book = swatch.cloneNode(true);
    book.classList.add("gathering-book");
    Object.assign(book.style, { left: `${rect.left}px`, top: `${rect.top}px` });
    document.body.append(book);
    book.animate([{ transform: "none", opacity: 1 }, { transform: `translate(${target.left + target.width / 2 - rect.left - rect.width / 2}px, ${target.top + 55 - rect.top - rect.height / 2}px) scale(.65) rotate(-10deg)`, opacity: 0 }], { duration: 300, easing: "cubic-bezier(.4,0,.4,1)" }).finished.finally(() => book.remove());
   });
  };
  const flyCart = (start, end, done, returning = false) => {
   if (reducedMotion()) { done(); return; }
   const flight = cartArt.cloneNode(true);
   flight.classList.add("cart-flight");
   flight.classList.toggle("is-returning", returning);
   flight.style.opacity = "1";
   flight.inert = true;
   const width = 200, height = 148;
   Object.assign(flight.style, { left: `${end.left + end.width / 2 - width / 2}px`, top: `${end.top + end.height / 2 - height / 2}px`, width: `${width}px`, height: `${height}px` });
   document.body.append(flight);
   cartFlight = flight;
   if (returning) flight.querySelector(".basket-books").animate([{ opacity: 0 }, { opacity: 1 }], { duration: 160, delay: 180, fill: "both" });
   document.body.classList.add("cart-travelling");
   const from = `translate(${start.left + start.width / 2 - end.left - end.width / 2}px, ${start.top + start.height / 2 - end.top - end.height / 2}px) scale(${start.width / width}, ${start.height / height})`;
   flight.animate([{ transform: from }, { transform: `scale(${end.width / width}, ${end.height / height})` }], { duration: 560, delay: returning ? 300 : 0, easing: "cubic-bezier(.22,.75,.18,1)", fill: "both" }).finished.then(() => {
    if (cartFlight !== flight) return;
    flight.remove();
    cartFlight = null;
    cartArt.style.opacity = "";
    document.body.classList.remove("cart-travelling");
    done();
   }).catch(() => {});
  };
  const openCart = () => {
   if (drawer.classList.contains("is-open")) return;
   returnFocus = previewFocus || document.activeElement;
   closeBook(false);
   const start = stopCartFlight() || toggle.getBoundingClientRect();
   drawer.inert = false;
   drawer.classList.add("is-open");
   drawer.setAttribute("aria-hidden", "false");
   toggle.setAttribute("aria-expanded", "true");
   scrim.hidden = false;
   document.body.classList.add("cart-open");
   backgroundInert(true);
   drawer.querySelector(".cart-close").focus();
   flyCart(start, tableCartRect(), () => {});
  };
  const closeCart = () => {
   if (!drawer.classList.contains("is-open")) return;
   const start = stopCartFlight() || tableCartRect();
   gatherBooks(start);
   drawer.inert = true;
   drawer.classList.remove("is-open");
   drawer.setAttribute("aria-hidden", "true");
   toggle.setAttribute("aria-expanded", "false");
   scrim.hidden = true;
   document.body.classList.remove("cart-open");
   backgroundInert(false);
   flyCart(start, toggle.getBoundingClientRect(), () => {
    (returnFocus?.isConnected ? returnFocus : toggle).focus();
    previewFocus = null;
   }, true);
  };
  const openBook = cover => {
   previewFocus = document.activeElement;
   const product = products.get(cover.closest(".product").dataset.productId);
   const know = skillsOf(product);
   backgroundInert(true);
   document.body.classList.add("cart-open");
   const rect = cover.getBoundingClientRect();
   const pagesHtml = `<div class="preview-pages"><button class="preview-close" aria-label="Close">×</button><p>${escapeHtml(product.description || "Handmade in Hawaii. Every Knowbook is unique. Open yours and experiment.")}</p>${chipSection("Learn:", know)}${chipSection("Includes:", toolsOf(product))}${pickupNote(product) ? `<p class="preview-note">In-person pickup only.</p>` : ""}<button class="preview-add" data-add="${escapeHtml(product.id)}">Add to cart <span>${money.format(product.priceUsd)}</span></button></div>`;
   const soundButtonHtml = `<button type="button" class="preview-sound" aria-label="Turn sound on" aria-pressed="false"><svg class="preview-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4z"/><line x1="17" y1="9" x2="22" y2="15"/><line x1="22" y1="9" x2="17" y2="15"/></svg><svg class="preview-loud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M19 5a9 9 0 0 1 0 14"/></svg></button>`;
   const coverHtml = product.video ? `<div class="preview-cover preview-flip"><div class="preview-face preview-front">${cover.outerHTML}</div><div class="preview-face preview-back"><video src="${escapeHtml(product.video)}" autoplay muted loop playsinline></video>${soundButtonHtml}</div></div>` : `${cover.outerHTML}`;
   preview.innerHTML = `<div class="preview-book${product.video ? " preview-book-video" : ""}">${pagesHtml}${coverHtml}</div>`;
   if (!product.video) preview.querySelector(".book-cover").classList.add("preview-cover");
   const soundButton = preview.querySelector(".preview-sound");
   if (soundButton) {
    const video = preview.querySelector(".preview-back video");
    let soundOn = false;
    try { soundOn = localStorage.getItem("knowbooks-sound") === "on"; } catch {}
    const setSound = on => {
     soundOn = on;
     video.muted = !on;
     soundButton.classList.toggle("is-on", on);
     soundButton.setAttribute("aria-pressed", String(on));
     soundButton.setAttribute("aria-label", on ? "Turn sound off" : "Turn sound on");
     if (on) { const playing = video.play(); if (playing?.catch) playing.catch(() => { video.muted = true; soundOn = false; soundButton.classList.remove("is-on"); soundButton.setAttribute("aria-pressed", "false"); soundButton.setAttribute("aria-label", "Turn sound on"); }); }
    };
    setSound(soundOn);
    soundButton.addEventListener("click", () => { try { localStorage.setItem("knowbooks-sound", soundOn ? "off" : "on"); } catch {} setSound(!soundOn); });
   }
   preview.hidden = false;
   const book = preview.firstElementChild;
   book.style.transition = "none";
   book.style.setProperty("--x", `${rect.left + rect.width / 2 - innerWidth / 2 - book.offsetWidth / 2}px`);
   book.style.setProperty("--y", `${rect.top + rect.height / 2 - innerHeight / 2}px`);
   book.style.setProperty("--scale", rect.width / book.offsetWidth);
   book.offsetWidth;
   book.style.removeProperty("transition");
   requestAnimationFrame(() => { preview.classList.add("is-open"); preview.querySelector(".preview-add").focus({ preventScroll: true }); });
  };
  const closeBook = (restoreFocus = true) => {
   if (preview.hidden) return;
   const url = new URL(location.href); url.searchParams.delete("knowbookId"); history.replaceState(null, "", url);
   preview.classList.remove("is-open"); preview.hidden = true;
   preview.querySelector("video")?.pause();
   if (!drawer.classList.contains("is-open")) { backgroundInert(false); document.body.classList.remove("cart-open"); }
   if (restoreFocus) { previewFocus?.focus(); previewFocus = null; }
  };
  const navigateBook = step => {
   const cards = [...grid.querySelectorAll(".product:not([hidden])")];
   if (cards.length < 2) return;
   const currentId = preview.querySelector(".preview-add")?.dataset.add;
   const current = cards.findIndex(card => card.dataset.productId === currentId);
   if (current === -1) return;
   const cover = cards[(current + step + cards.length) % cards.length].querySelector(".book-cover");
   if (!cover) return;
   closeBook(false);
   cover.click();
   previewFocus = cover.querySelector("a") || cover;
  };
  const addToCart = async (id, source, button) => {
   if (adding) return;
   adding = true;
   button.disabled = true;
   const rect = source.getBoundingClientRect();
   const product = products.get(id);
   closeBook(false);
   if (!reducedMotion()) {
    const flight = document.createElement("div");
    flight.className = "flying-book";
    flight.inert = true;
    flight.setAttribute("aria-hidden", "true");
    flight.innerHTML = bookCover(product);
    Object.assign(flight.style, { left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
    document.body.append(flight);
    const target = toggle.getBoundingClientRect();
    await flight.animate([{ transform: "none", opacity: 1 }, { transform: `translate(${target.left + target.width / 2 - rect.left - rect.width / 2}px, ${target.top + target.height / 2 - rect.top - rect.height / 2}px) scale(.08) rotate(12deg)`, opacity: .3 }], { duration: 680, easing: "cubic-bezier(.45,0,.25,1)" }).finished;
    flight.remove();
   }
   change(id, 1);
   button.disabled = false;
   adding = false;
   (previewFocus?.isConnected ? previewFocus : button).focus();
   previewFocus = null;
  };
  const change = (id, delta) => {
   const focused = document.activeElement;
   const control = focused?.hasAttribute("data-more") ? "data-more" : focused?.hasAttribute("data-less") ? "data-less" : null;
   const next = Math.min(products.get(id).design ? 1 : Infinity, (cart.get(id) || 0) + delta);
   next > 0 ? cart.set(id, next) : cart.delete(id);
   refresh();
   if (control) {
    const replacement = [...lines.querySelectorAll(`[${control}]`)].find(button => button.getAttribute(control) === id);
    (replacement || drawer.querySelector(".cart-close")).focus();
   }
  };
  function initDesigner() {
   const $ = selector => document.querySelector(selector), dialog = $('#designer'), title = $('#design-title'), signature = $('#design-signature'), signatureArt = $('#design-signature-art');
   const art = $('#design-art'), marker = $('#design-marker'), status = $('#design-status');
   const key = 'knowbooks-signature-design', undo = [], hedcuts = new WeakMap();
   let design, draft, editing = false, erase = false, drawing, opener, photoVersion = 0;
   const fresh = () => ({ id: `custom-${cryptoId()}`, title: 'Your Knowbook', signature: '', color: 'navy', superpowers: '', art: [], portrait: [], svgImage: '', coverDescription: '' });
   function cryptoId() { return globalThis.crypto.randomUUID().slice(0, 8); }
   const covers = window.KnowbookCovers;
   const validSvgImage = covers.validSvgImage;
   const body = source => covers.compile(source).replace(/^<svg[^>]*>|<\/svg>$/g,'');
   const paths = values => body(values.map(d=>'path '+d).join('\n'));
   const dots = values => body(covers.packDots(values).join('\n'));
   const vectorImage = value => value ? body('svgImage '+value) : '';
   const cover = d => covers.compile(covers.designerSource(d));
   const product = d => ({ id: d.id, title: `${d.title} — Signature design deposit (${d.id})`, priceUsd: 5000, coverColor: coverPalette[d.color], cover: cover(d), design: d });
   const remember = () => {
    design.title = title.textContent.replace(/\s+/g,' ').trim().slice(0, 60);
    design.signature = signature.value.trim();
    syncCode();
    if (editing) return;
    draft = design;
    try { localStorage.setItem(key, JSON.stringify(draft)); } catch { status.textContent = 'Draft could not be saved on this device. Save your design using the code panel before leaving.'; }
   };
   // Rebuild saved custom products from validated design data, never stored HTML.
   const restoreDesign = d => {
    if (!d || !/^custom-[a-f0-9-]{8}$/.test(d.id) || !Object.hasOwn(coverPalette,d.color)) return null;
    d.title = String(d.title).slice(0,60); d.signature = String(typeof d.signature === 'string' ? d.signature : '').replace(/\s+/g,' ').slice(0,60);
    d.superpowers = String(d.superpowers ?? (Array.isArray(d.verbs) ? d.verbs.filter(Boolean).join(' ') : '')).trim().split(/\s+/).slice(0,5).join(' ').slice(0,124);
    delete d.verbs;
    d.coverDescription = String(d.coverDescription || d.symbol || '').replace(/\s+/g,' ').trim();
    delete d.symbol;
    d.art = (Array.isArray(d.art) ? d.art : []).filter(p => typeof p === 'string' && p.length < 20000 && /^[MLQ0-9., -]+$/.test(p) && !/\.\d{2}/.test(p)).slice(0,200);
    d.portrait = (Array.isArray(d.portrait) ? d.portrait : []).filter(p => Array.isArray(p) && p.length === 3 && p.every(n=>Number.isFinite(n) && /^-?\d+(?:\.\d)?$/.test(String(n))) && p[0]>=0 && p[0]<=256 && p[1]>=0 && p[1]<=256 && p[2]>0 && p[2]<4).slice(0,4500);
    d.svgImage = validSvgImage(d.svgImage) ? d.svgImage : '';
    return d;
   };
   try {
    draft = restoreDesign(JSON.parse(localStorage.getItem(key) || 'null'));
    if(draft) products.set(draft.id, product(draft));
   } catch {}
   try {
    const saved = JSON.parse(localStorage.getItem('knowbooks-cart') || '[]');
    for(const [id, quantity, data] of saved) {
     const d = restoreDesign(data);
     if(d && d.id === id && Number.isInteger(quantity) && quantity > 0) products.set(id, product(d));
    }
   } catch {}
   const proposal = d => {
    const leaf = value => String(value || '').replace(/\s+/g,' ').trim();
    return [
     `title ${leaf(d.title)}`, ...(d.signature ? [`signature ${leaf(d.signature)}`] : []), `color ${d.color}`,
     'designFeeUsd 10000', 'depositUsd 5000', 'prototype One shipped within 30 days of deposit', 'balance Due before shipment',
     'copies Our production cost + 30%',
     `superpowers ${leaf(d.superpowers)}`,
     ...(d.coverDescription ? [`coverDescription ${leaf(d.coverDescription)}`] : []),
     'cover', ...covers.designerSource(d).split('\n').map(line=>' '+line)
    ].join('\n') + '\n';
   };
   designToScroll = proposal;
   restoreDesignFromData = restoreDesign;
   productFromDesign = product;
   const code = $('#design-code'), codeStatus = $('#design-code-status'), designLink = $('#design-link');
   designLink.addEventListener('focus', () => designLink.select());
   let codeInvalid=false;
   function syncCode(fromEditor=false) {
    if(codeInvalid || (!fromEditor && document.activeElement===code)) return;
    const text=fromEditor ? code.value.replace(/\n?$/, '\n') : proposal(design);
    const [start,end]=[code.selectionStart,code.selectionEnd]; code.value=text; if(fromEditor) code.setSelectionRange(start,end); codeInvalid=false; code.setCustomValidity(''); codeStatus.textContent='';
    const url=new URL(location.href);url.search='';url.hash='knowbook='+encodeURIComponent(text);designLink.value=url.href;
   }
   function readProposal(text) {
    const root={children:[]}, stack=[root];
    const fail = message => {throw Error(message);};
    for(const line of text.replace(/\r\n/g,'\n').split('\n').filter(line=>line.trim())) {
     const match=line.match(/^( *)(\S+)(?: (.*))?$/); if(!match || line.includes('\t')) fail('Use spaces for indentation.');
     const [,spaces,key,value='']=match, depth=spaces.length;
     if(depth>=stack.length) fail('Indent one space per level.');
     const node={key,value,children:[]};stack[depth].children.push(node);stack[depth+1]=node;stack.length=depth+2;
    }
    const fields = (node, allowed) => {
     const result={};for(const child of node.children) {if(!allowed.includes(child.key) || result[child.key]) fail(`Unexpected or repeated field: ${child.key}`);result[child.key]=child;}return result;
    };
    const scalar = (node, fallback='') => {if(node?.children.length) fail(`Unexpected indentation below ${node.key}.`);return node?.value ?? fallback;};
    const top=fields(root,['title','signature','color','designFeeUsd','depositUsd','prototype','balance','copies','superpowers','experiments','coverDescription','symbol','cover']);
    const d={...fresh(),title:scalar(top.title),id:design.id,signature:scalar(top.signature),color:scalar(top.color),coverDescription:scalar(top.coverDescription,scalar(top.symbol)),superpowers:scalar(top.superpowers,scalar(top.experiments))};
    if(!d.title.trim() || d.title.length>60 || d.signature.length>60) fail('Use a title and signature of at most 60 characters.');
    if(!Object.hasOwn(coverPalette,d.color)) fail('Use a listed cover color.');
    if(d.superpowers.trim().split(/\s+/).length>5 || d.superpowers.length>124) fail('Use up to 5 space-separated words (124 characters maximum).');
    for(const line of proposal(d).split('\n').filter(line=>/^(designFeeUsd|depositUsd|prototype|balance|copies) /.test(line))) {
     const space=line.indexOf(' '), key=line.slice(0,space);if(scalar(top[key])!==line.slice(space+1)) fail(`${key} is a fixed offer term.`);
    }
    if(!top.cover || top.cover.value) fail('Include the cover tree.');
    const serialize = (node,depth=0) => [' '.repeat(depth)+node.key+(node.value?' '+node.value:''), ...node.children.flatMap(child=>serialize(child,depth+1))];
    Object.assign(d,covers.readDesigner(top.cover.children.flatMap(node=>serialize(node)).join('\n')));
    return d;
   }
   code.addEventListener('input', () => {
    try {const next=readProposal(code.value);photoVersion++;undo.length=0;design=next;codeInvalid=false;showDesign();remember();code.setCustomValidity('');codeStatus.textContent='';syncCode(true);}
    catch(error) {codeInvalid=true;designLink.value='';code.setCustomValidity(error.message);codeStatus.textContent=error.message;}
   });
   code.addEventListener('blur', () => {if(!codeInvalid) syncCode();});
   lines.addEventListener('click', event => {
    const button = event.target.closest('[data-edit-design]'); if (!button) return;
    const saved = products.get(button.dataset.editDesign).design;
    open(event, saved);
   });
   const superpowers = $('#design-superpowers');
   superpowers.addEventListener('input', () => {
    const words=superpowers.value.trim().split(/\s+/);
    if(words.length>5) superpowers.value=words.slice(0,5).join(' ')+' ';
    design.superpowers=superpowers.value; remember();
   });
   $('#design-colors').innerHTML = Object.entries(coverPalette).map(([name,color]) => `<button type="button" style="--swatch:${color}" data-color="${name}" aria-label="${name}" title="${name}" aria-pressed="false"></button>`).join('');
   function render() {
    $('.design-cover').style.setProperty('--cover', coverPalette[design.color]);
    $('#design-colors').querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed',b.dataset.color === design.color));
    $('#design-portrait').innerHTML = vectorImage(design.svgImage) + dots(design.portrait); $('#remove-image').hidden = !design.svgImage && !design.portrait.length && !hedcuts.has(design);
    art.querySelector('.strokes').innerHTML = paths(design.art);
    signatureArt.innerHTML = signatureFor(design.title, design.signature);
   }
   $('#design-colors').onclick = event => { const b = event.target.closest('[data-color]'); if(b) { design.color = b.dataset.color; render(); remember(); } };
   title.addEventListener('keydown', event => { if(event.key === 'Enter') event.preventDefault(); });
   title.addEventListener('input', () => { if(title.textContent.length > 60) title.textContent = title.textContent.slice(0,60); remember(); signatureArt.innerHTML = signatureFor(design.title, design.signature); });
   signature.addEventListener('input', () => { remember(); signatureArt.innerHTML = signatureFor(design.title, design.signature); });
   marker.onclick = () => { erase = !erase; marker.setAttribute('aria-pressed',erase); marker.setAttribute('aria-label',erase ? 'Flip eraser to marker' : 'Flip marker to eraser'); marker.querySelector('span').textContent = erase ? 'Flip to draw' : 'Flip to erase'; };
   const snapshot = () => { undo.push(JSON.stringify(design.art)); if(undo.length>30) undo.shift(); };
   $('#design-undo').onclick = () => { if(undo.length) { design.art = JSON.parse(undo.pop()); render(); remember(); } };
   // Reduce a stroke to its bends, then round them. Nearly straight strokes become a single line.
   const distance = (p,a,b) => { const dx=b[0]-a[0],dy=b[1]-a[1], t=Math.max(0,Math.min(1,((p[0]-a[0])*dx+(p[1]-a[1])*dy)/(dx*dx+dy*dy || 1))); return Math.hypot(p[0]-a[0]-t*dx,p[1]-a[1]-t*dy); };
   const simplify = points => {
    let max=2.5,index=0; for(let i=1;i<points.length-1;i++) { const d=distance(points[i],points[0],points.at(-1)); if(d>max) {max=d;index=i;} }
    return index ? [...simplify(points.slice(0,index+1)).slice(0,-1),...simplify(points.slice(index))] : [points[0],points.at(-1)];
   };
   const smooth = points => {
    const p=simplify(points), last=p.at(-1); let d=`M${p[0]}`;
    for(let i=1;i<p.length-1;i++) d+=` Q${p[i]} ${(p[i][0]+p[i+1][0])/2},${(p[i][1]+p[i+1][1])/2}`;
    return `${d} L${last}`;
   };
   [art].forEach(surface => {
    const field='art';
    const point = e => { const p=new DOMPoint(e.clientX,e.clientY).matrixTransform(surface.getScreenCTM().inverse()); return [Math.round(Math.max(0,Math.min(256,p.x))),Math.round(Math.max(0,Math.min(surface.viewBox.baseVal.height,p.y)))]; };
    const remove = e => {
     const hit=document.elementFromPoint(e.clientX,e.clientY), group=surface.querySelector('.strokes');
     if(hit?.parentNode === group) { design[field].splice([...group.children].indexOf(hit),1); render(); }
    };
    surface.onpointerdown = e => {
     if(drawing || (e.button !== 0 && e.button !== 5)) return;
     e.preventDefault(); snapshot(); surface.setPointerCapture(e.pointerId);
     drawing={id:e.pointerId, points:[point(e)], erase:erase || e.button===5};
     if(drawing.erase) remove(e);
     else { const path=document.createElementNS('http://www.w3.org/2000/svg','path'); surface.querySelector('.strokes').append(path); drawing.path=path; }
    };
    surface.onpointermove = e => {
     if(!drawing || drawing.id!==e.pointerId) return;
     if(drawing.erase) {remove(e);return;}
     const p=point(e); if(Math.hypot(p[0]-drawing.points.at(-1)[0],p[1]-drawing.points.at(-1)[1])<1.5 || drawing.points.length>=1500) return;
     drawing.points.push(p); drawing.path.setAttribute('d','M'+drawing.points.join(' L'));
    };
    surface.onpointerup = e => {
     if(!drawing || drawing.id!==e.pointerId) return;
     if(!drawing.erase) { drawing.points.push(point(e)); design[field].push(smooth(drawing.points)); }
     drawing=null; surface.releasePointerCapture(e.pointerId); render(); remember();
    };
    surface.onpointercancel = () => { if(drawing) { design.art=JSON.parse(undo.pop()); drawing=null; render(); } };
   });
   $('#remove-image').onclick = () => { photoVersion++; hedcuts.delete(design); design.portrait=[]; design.svgImage=''; $('#portrait-file').value=''; render(); remember(); };
   // Keep original tones so repeated adjustments never degrade an uploaded image.
   art.addEventListener('keydown', event => {
    if(event.ctrlKey || event.metaKey || event.altKey || !['i','[',']','0'].includes(event.key.toLowerCase())) return;
    if(!design.portrait.length && !hedcuts.has(design)) return;
    event.preventDefault();
    if(!hedcuts.has(design)) {
     const radii=new Map(design.portrait.map(([x,y,r])=>[`${x},${y}`,r/2])), tones=[];
     for(let y=4;y<256;y+=4) for(let x=4;x<256;x+=4) tones.push([x,y,radii.get(`${x},${y}`) || 0]);
     hedcuts.set(design,{tones,original:structuredClone(design.portrait),inverse:false,gamma:1});
    }
    const h=hedcuts.get(design), key=event.key.toLowerCase();
    if(key==='i') h.inverse=!h.inverse;
    else if(key==='0') {h.inverse=false;h.gamma=1;}
    else h.gamma=Math.max(.25,Math.min(4,h.gamma*(key==='[' ? 1.25 : .8)));
    applyHedcut(h);
   });
   function applyHedcut(h) {
    design.portrait=!h.inverse && h.gamma===1 ? structuredClone(h.original) : covers.tonesToDots(h.tones,h.inverse,h.gamma);
    render(); remember();
   }
   const isSvgFile = file => file.type === 'image/svg+xml' || /\.svg$/i.test(file.name);
   const isCoverImage = file => isSvgFile(file) || ['image/png','image/jpeg','image/webp','image/gif'].includes(file.type);
   const useCoverImage = async file => {
    const version=++photoVersion;
    if(!file) return;
    if(!isCoverImage(file)) {status.textContent='Choose a PNG, JPEG, WebP, GIF or SVG image.';return;}
    try {
     if(isSvgFile(file)) {
      const source=await file.text(), doc=new DOMParser().parseFromString(source, 'image/svg+xml');
      if(doc.querySelector('parsererror') || doc.documentElement.localName!=='svg' || doc.documentElement.namespaceURI!=='http://www.w3.org/2000/svg') throw Error('Invalid SVG');
      const data=await new Promise((resolve,reject) => { const reader=new FileReader(); reader.onload=()=>resolve(reader.result); reader.onerror=()=>reject(reader.error); reader.readAsDataURL(new Blob([source],{type:'image/svg+xml'})); });
      const image=new Image(); image.src=data; await image.decode();
      if(version!==photoVersion) return;
      design.coverDescription ||= `generated from ${file.name}`;
      design.svgImage=data; design.portrait=[]; design.art=[]; hedcuts.delete(design); undo.length=0;
      status.textContent=''; render(); remember(); art.focus(); return;
     }
     const image=await createImageBitmap(file); if(version!==photoVersion) {image.close();return;}
     const canvas=document.createElement('canvas'); canvas.width=canvas.height=256;
     const ctx=canvas.getContext('2d'); ctx.fillStyle='white'; ctx.fillRect(0,0,256,256);
     const size=Math.min(image.width,image.height); ctx.drawImage(image,(image.width-size)/2,(image.height-size)/2,size,size,0,0,256,256); image.close();
     design.coverDescription ||= `generated from ${file.name}`;
     const tones=covers.tonesFromPixels(ctx.getImageData(0,0,256,256).data);
     design.portrait=covers.tonesToDots(tones);
     hedcuts.set(design,{tones,original:structuredClone(design.portrait),inverse:false,gamma:1});
     design.svgImage=''; design.art=[]; undo.length=0; status.textContent=''; render(); remember(); art.focus();
    } catch {if(version===photoVersion) status.textContent='Could not read that image. Try another PNG, JPEG, WebP, GIF or SVG.';}
   };
   $('#portrait-file').onchange = event => useCoverImage(event.target.files[0]);
   const dropTarget = document.createElement('div');
   dropTarget.className='cover-drop-target'; dropTarget.hidden=true; dropTarget.setAttribute('role','status');
   dropTarget.textContent='Drop file to use as custom knowbook cover art';
   document.body.append(dropTarget);
   let dragDepth=0;
   const isFileDrag = event => [...(event.dataTransfer?.types || [])].includes('Files');
   const clearDropTarget = () => {dragDepth=0;dropTarget.hidden=true;};
   const showDropTarget = () => {
    // A modal dialog is in the browser's top layer, so host the overlay there when open.
    const host=dialog.open ? dialog : document.body;
    if(dropTarget.parentNode!==host) host.append(dropTarget);
    dropTarget.hidden=false;
   };
   document.addEventListener('dragenter', event => {
    if(!isFileDrag(event)) return;
    event.preventDefault(); dragDepth++; showDropTarget();
   });
   document.addEventListener('dragover', event => {
    if(!isFileDrag(event)) return;
    event.preventDefault(); event.dataTransfer.dropEffect='copy'; showDropTarget();
   });
   document.addEventListener('dragleave', event => {
    if(!isFileDrag(event) && !dragDepth) return;
    if(--dragDepth<=0 || !event.relatedTarget) clearDropTarget();
   });
   const openCoverImage = file => {
    if(!dialog.open) {
     const next=fresh();
     next.title=file.name.replace(/\.[^.]+$/, '').replace(/[_-]+/g,' ').replace(/\s+/g,' ').trim().slice(0,60) || next.title;
     open(undefined,undefined,next);
    }
    useCoverImage(file);
   };
   document.addEventListener('drop', event => {
    clearDropTarget();
    if(!isFileDrag(event)) return;
    event.preventDefault();
    const file=[...event.dataTransfer.files].find(isCoverImage);
    if(!file) {if(dialog.open) status.textContent='Choose a PNG, JPEG, WebP, GIF or SVG image.';return;}
    openCoverImage(file);
   });
   document.addEventListener('paste', event => {
    const file=[...(event.clipboardData?.files || [])].find(isCoverImage);
    if(!file) return;
    event.preventDefault();
    openCoverImage(file);
   });
   window.addEventListener('blur',clearDropTarget);
   document.addEventListener('dragend',clearDropTarget);
   function showDesign() {
    title.textContent=design.title; signature.value=design.signature || '';
    superpowers.value=design.superpowers;
    $('#design-add').textContent=editing ? 'Update cart' : 'Add $5,000 deposit to cart';
    render();
   }
   function open(event,existing,newDesign) {
    codeInvalid=false;event?.preventDefault(); opener=event?.target.closest('button, a') || $('#signature-element-13'); editing=Boolean(existing); photoVersion++; undo.length=0;
    design=existing ? structuredClone(existing) : newDesign || draft || fresh();
    showDesign(); syncCode();
    status.textContent=''; $('#portrait-file').value=''; render(); dialog.showModal(); title.focus();
   }
   ['#signature-element-13','#startups-element-13','#training-element-6'].forEach(selector => { $(selector).onclick = event => open(event); });
   document.addEventListener('keydown', event => {
    if(!['d','c'].includes(event.key) || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey || event.repeat || event.isComposing || event.defaultPrevented) return;
    if(event.target.isContentEditable || event.target.closest('input, textarea, select, [role="textbox"]') || document.querySelector('dialog[open]')) return;
    event.preventDefault();
    event.key === 'c' ? openCart() : (closeCart(), closeBook(), open());
   });
   $('#design-close').onclick = () => dialog.close();
   dialog.addEventListener('click', event => { if(event.target===dialog) { const r=dialog.getBoundingClientRect(); if(event.clientX<r.left || event.clientX>r.right || event.clientY<r.top || event.clientY>r.bottom) dialog.close(); } });
   dialog.addEventListener('close', () => {photoVersion++; remember(); editing=false; (opener?.isConnected ? opener : drawer.querySelector('.cart-close')).focus();});
   $('#design-form').onsubmit = event => {
    event.preventDefault(); remember();
    if(!design.title) {status.textContent='Give your Knowbook a title.';title.focus();return;}
    const previousProduct=products.get(design.id), previousQuantity=cart.get(design.id);
    products.set(design.id,product(structuredClone(design))); cart.set(design.id,1);
    if(!refresh()) {
     if(previousProduct) products.set(design.id,previousProduct); else products.delete(design.id);
     if(previousQuantity) cart.set(design.id,previousQuantity); else cart.delete(design.id);
     refresh(); status.textContent='Could not save your cart on this device. Your design is still here; save it using the code panel or free up browser storage and try again.'; return;
    }
    if(!editing) { design=fresh(); draft=design; showDesign(); syncCode(); }
    dialog.close(); openCart();
   };
   const loadDesignLink = () => {
    if(!location.hash.startsWith('#knowbook=')) return;
    open();$('#design-source').open=true;
    try {code.value=decodeURIComponent(location.hash.slice('#knowbook='.length));code.dispatchEvent(new Event('input'));}
    catch {code.value='';codeInvalid=true;designLink.value='';codeStatus.textContent='This design link has invalid URL encoding.';code.setCustomValidity(codeStatus.textContent);}
   };
   window.addEventListener('hashchange',loadDesignLink);loadDesignLink();
  }
  productCatalog.forEach(product => {
   const shelves = shelvesOf(product);
   if (!shelves.length || new Set(shelves).size !== shelves.length) throw new Error(`Invalid shelves for ${product.id}`);
   shelves.forEach(editionDetails);
  });
  grid.innerHTML = shelfOrder.flatMap(shelf => {
   const stageProducts = productCatalog.filter(product => shelvesOf(product).includes(shelf));
   return stageProducts.length ? [editionDivider(shelf), ...stageProducts.map(product => productCard(product, shelf))] : [];
  }).join("");
  const catalogNodes = Array.from(grid.children);
  const restoreSearch = () => {
   document.querySelector("#catalog-search").value = new URLSearchParams(location.search).get("q") || "";
   filterCatalog();
  };
  restoreSearch();
  window.addEventListener("popstate", restoreSearch);
  const linkedBook = document.getElementById(new URLSearchParams(location.search).get("knowbookId"));
  if (linkedBook?.classList.contains("product") && !document.querySelector("#catalog-search").value.trim()) {
   collapsedEditions.delete(linkedBook.dataset.shelf);
   filterCatalog();
  }
  if (linkedBook && !linkedBook.hidden) { document.addEventListener("scrollend", () => openBook(linkedBook.querySelector(".book-cover")), { once: true }); linkedBook.scrollIntoView(); }
  else if (document.querySelector("#catalog-search").value.trim()) {
   requestAnimationFrame(() => document.querySelector("#catalog").scrollIntoView({ behavior: "instant", block: "start" }));
  }
  fitBookTitles();
  initDesigner();
  document.querySelector("#partner-buy").addEventListener("click", () => {
   document.querySelector("#partner-dialog").close();
   change("knowbook", 1);
   openCart();
  });
  try {
   const saved = JSON.parse(localStorage.getItem("knowbooks-cart") || "[]");
   saved.forEach(([id, quantity]) => { if (products.has(id) && Number.isInteger(quantity) && quantity > 0) cart.set(id, products.get(id).design ? 1 : quantity); });
  } catch {}
  grid.addEventListener("click", event => {
   const link = event.target.closest(".edition-toggle");
   if (!link) return;
   event.preventDefault();
   const shelf = link.closest(".edition-divider").dataset.shelf;
   collapsedEditions.has(shelf) ? collapsedEditions.delete(shelf) : collapsedEditions.add(shelf);
   filterCatalog();
   link.focus({ preventScroll: true });
  });
  grid.addEventListener("keydown", event => {
   if (event.key === " " && event.target.matches(".edition-toggle")) { event.preventDefault(); event.target.click(); }
  });
  grid.addEventListener("click", event => { const cover = event.target.closest(".book-cover"); const button = event.target.closest("[data-add]"); if (cover) { const url = new URL(location.href); url.searchParams.set("knowbookId", cover.closest(".product").dataset.productId); history.replaceState(null, "", url); openBook(cover); } if (button) { addToCart(button.dataset.add, button.closest(".product").querySelector(".book-cover"), button); } });
  preview.addEventListener("click", event => { const button = event.target.closest(".preview-add"); if (button) addToCart(button.dataset.add, preview.querySelector(".preview-book"), button); if (event.target === preview || event.target.closest(".preview-close")) closeBook(); });
  lines.addEventListener("click", event => { const more = event.target.closest("[data-more]"); const less = event.target.closest("[data-less]"); if (more) change(more.dataset.more, 1); if (less) change(less.dataset.less, -1); });
  referralInput.addEventListener("input", refresh);
  toggle.addEventListener("click", openCart);
  drawer.querySelector(".cart-close").addEventListener("click", closeCart);
  empty.querySelector("a").addEventListener("click", closeCart);
  scrim.addEventListener("click", closeCart);
  document.addEventListener("keydown", event => {
   if (document.querySelector("#designer").open) return;
   if (event.key === "Escape") { closeCart(); closeBook(); }
   if (!preview.hidden && (event.key === "ArrowLeft" || event.key === "ArrowRight") && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.repeat && !event.isComposing) {
    event.preventDefault();
    navigateBook(event.key === "ArrowRight" ? 1 : -1);
   }
   const modal = drawer.classList.contains("is-open") ? drawer : !preview.hidden ? preview : null;
   if (event.key !== "Tab" || !modal) return;
   const focusable = [...modal.querySelectorAll("button:not(:disabled), input:not(:disabled), a[href], summary")].filter(element => element.getClientRects().length);
   const first = focusable[0], last = focusable.at(-1);
   if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
   else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
  });
  document.querySelector("#catalog-search").addEventListener("input", filterCatalog);
  window.addEventListener("resize", fitBookTitles);
  refresh();
  try {
   const order = location.hash.startsWith("#order=") ? location.hash.slice(7) : "";
   if (order) {
    JSON.parse(fromBase64Url(order.slice(7))).forEach(([id, quantity, data]) => {
     const restored = data && restoreDesignFromData(data);
     if (restored && restored.id === id) { products.set(id, productFromDesign(restored)); cart.set(id, 1); }
     else if (products.has(id) && Number.isInteger(quantity) && quantity > 0) cart.set(id, products.get(id).design ? 1 : quantity);
    });
    refresh();
    openCart();
   }
  } catch {}
 })();

catalog.scroll
buildCoverRuntime
buildConcepts catalog.js
buildZip knowbooks.zip index.html signature.js catalog.js covers.js catalog.csv tags.js gitHash.js knowbooksLogo.svg og.png hero-background.png kbc.jpg readme.scroll catalog.scroll catalogCsv.scroll covers.scroll tags.scroll partners.scroll upstreamToScroll.scroll s/checko s/qrcode

About

Knowbooks are books you do.

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages