Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
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"> </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 ↗ 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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); // 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