diff --git a/AGENTS.md b/AGENTS.md
index d638863..fdaf29c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -359,7 +359,12 @@ session with **no other memory of this project**. So:
only previews what's coming. **Do not add prices/fake urgency/buy buttons until
there is real stock**; when items exist, model them as listings and take
Lightning/Cashu payment. The page reads the live stock strip as 0/None/Nothing
- — keep it honest if that changes.
+ — keep it honest if that changes. It also serves two **free printables**
+ (`public/images/print/skatespots-sticker.svg` 55×86 mm, `…-poster.svg` A3),
+ regenerated with `node scripts/make-print-art.mjs` (output is committed). Those
+ SVGs are self-contained: brand fonts + logo embedded as data URIs and a real
+ vector QR (verified decoding to https://skate-spots.org — if you regenerate,
+ re-verify the QR scans with the logo in the middle; error-correction level H).
- **Mints directory**: `/mints` page lists Cashu mints from the community
**MintRadar** project. MintRadar has **no CORS**, so it's fetched same-origin
at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* →
diff --git a/public/images/print/skatespots-poster.svg b/public/images/print/skatespots-poster.svg
new file mode 100644
index 0000000..ddec04e
--- /dev/null
+++ b/public/images/print/skatespots-poster.svg
@@ -0,0 +1,48 @@
+
diff --git a/public/images/print/skatespots-sticker.svg b/public/images/print/skatespots-sticker.svg
new file mode 100644
index 0000000..969f9e5
--- /dev/null
+++ b/public/images/print/skatespots-sticker.svg
@@ -0,0 +1,26 @@
+
diff --git a/release-notes.txt b/release-notes.txt
index a68dd81..58be796 100644
--- a/release-notes.txt
+++ b/release-notes.txt
@@ -1,6 +1,8 @@
v1.5.4
-🛍 App Shop — a new section (empty on purpose)
+🛍 App Shop — a new section (empty on purpose, with free printables)
+- Free to print, no sats needed: a 55 × 86 mm "DO YOU EVEN SKATE BRO?" sticker
+ and an A3 poster, both print-ready vector files with a working QR.
- Merch is coming, and it'll be paid in sats: Lightning or Cashu, your choice.
No cards, no accounts, no processor skimming the drop.
- To be clear: we hold no stock and nothing is for sale yet. No pre-orders, no
diff --git a/scripts/make-print-art.mjs b/scripts/make-print-art.mjs
new file mode 100644
index 0000000..0903f30
--- /dev/null
+++ b/scripts/make-print-art.mjs
@@ -0,0 +1,165 @@
+#!/usr/bin/env node
+/**
+ * Generates the print-ready VECTOR artwork served by the App Shop:
+ * run with `node scripts/make-print-art.mjs` (output is committed, so this is
+ * only needed when the artwork changes).
+ * public/images/print/skatespots-sticker.svg (55 × 86 mm)
+ * public/images/print/skatespots-poster.svg (A3, 297 × 420 mm)
+ *
+ * Self-contained: brand fonts (Outfit variable + Barlow Condensed) and the logo
+ * are embedded as data URIs, and the QR is real vector from `qrcode`. That means
+ * it prints crisp at any size and needs no network.
+ */
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { createRequire } from 'node:module';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const require = createRequire(join(ROOT, 'package.json'));
+const QRCode = require('qrcode');
+
+const OUT = join(ROOT, 'public/images/print');
+mkdirSync(OUT, { recursive: true });
+
+const ORANGE = '#f08c2a';
+const WHITE = '#ffffff';
+const BG = '#0a0a0a';
+const QR_URL = 'https://skate-spots.org';
+
+// ── assets ───────────────────────────────────────────────────────────────────
+const b64 = (p) => readFileSync(p).toString('base64');
+const outfit = b64(join(ROOT, 'node_modules/@fontsource-variable/outfit/files/outfit-latin-wght-normal.woff2'));
+const barlow = b64(join(ROOT, 'node_modules/@fontsource/barlow-condensed/files/barlow-condensed-latin-600-normal.woff2'));
+const logoPng = b64(join(ROOT, 'public/images/icon-1024.png'));
+
+const FONTS = `
+ `;
+
+/** Real QR module path (no background) + its viewBox size. */
+async function qrPath() {
+ const svg = await QRCode.toString(QR_URL, {
+ type: 'svg',
+ margin: 0,
+ errorCorrectionLevel: 'H',
+ color: { dark: '#000000', light: '#ffffff' },
+ });
+ const m = svg.match(/]*d="([^"]+)"[^>]*\/?>/g) || [];
+ // The first path is the solid background; the rest are the modules.
+ const modulePath = (svg.match(/
+
+
+
+
+ `;
+}
+
+/** Block-shadowed headline: orange offset copy behind a white one. */
+function headline({ text, cx, y, size, width, offset, stroke = 0, letterSpacing = '0.01em' }) {
+ const common = `text-anchor="middle" x="${cx}" y="${y}" font-size="${size}" letter-spacing="${letterSpacing}"` +
+ (width ? ` textLength="${width}" lengthAdjust="spacingAndGlyphs"` : '');
+ return `
+ ${text}
+ ${text}`;
+}
+
+/** Sawtooth ribbon of alternating triangles. dir: 'down' means apex points down. */
+function ribbon({ y, width, height, dir, count = 13 }) {
+ const w = width / count;
+ let out = '';
+ for (let i = 0; i < count; i++) {
+ const x = i * w;
+ const fill = i % 2 === 0 ? ORANGE : WHITE;
+ const pts =
+ dir === 'down'
+ ? `${x},${y} ${x + w / 2},${y + height} ${x + w},${y}`
+ : `${x},${y + height} ${x + w / 2},${y} ${x + w},${y + height}`;
+ out += ``;
+ }
+ return out;
+}
+
+const dashed = ({ y, x1, x2, color = '#3a3a3a', w = 4 }) =>
+ ``;
+
+// ── sticker (portrait, 55 × 86 mm at 0.1 mm units) ───────────────────────────
+async function sticker() {
+ const qr = await qrPath();
+ const W = 550, H = 860;
+ const qrSize = 400;
+ return `\n`;
+}
+
+// ── poster (A3 portrait, 0.1 mm units → 2970 × 4200) ─────────────────────────
+async function poster() {
+ const qr = await qrPath();
+ const W = 2970, H = 4200;
+ const qrSize = 1500;
+ const cx = W / 2;
+ return `\n`;
+}
+
+const s = await sticker();
+const p = await poster();
+writeFileSync(join(OUT, 'skatespots-sticker.svg'), s);
+writeFileSync(join(OUT, 'skatespots-poster.svg'), p);
+console.log('sticker:', s.length, 'bytes');
+console.log('poster :', p.length, 'bytes');
+console.log('→', OUT);
diff --git a/src/pages/AppShopPage.tsx b/src/pages/AppShopPage.tsx
index 0c05efd..429ea95 100644
--- a/src/pages/AppShopPage.tsx
+++ b/src/pages/AppShopPage.tsx
@@ -13,6 +13,9 @@ import {
MapPin,
MessageCircle,
ArrowRight,
+ Printer,
+ Download,
+ Scissors,
} from 'lucide-react';
import { PageHeader } from '@/components/PageHeader';
import { NewsletterSubscribeButton } from '@/components/NewsletterSubscribeButton';
@@ -33,6 +36,26 @@ const REQUEST_HREF = `/messages/${SHOP_OWNER_NPUB}`;
* Lightning/Cashu payment; until then this page must never imply a sale.
*/
+/**
+ * Free, printable artwork. Physical size is baked into the files (mm units), so
+ * "Actual size" printing gives an exact 55 × 86 mm sticker / A3 poster, and the
+ * QR in each one resolves to https://skate-spots.org.
+ */
+const FREE_PRINT = [
+ {
+ title: 'Sticker',
+ size: '55 × 86 mm',
+ tip: 'Black die-cut. Print on sticker paper, cut along the black edge.',
+ file: '/images/print/skatespots-sticker.svg',
+ },
+ {
+ title: 'Poster',
+ size: 'A3 · 297 × 420 mm',
+ tip: 'Prints on A3; shrink to A4 (50%) and it still scans fine.',
+ file: '/images/print/skatespots-poster.svg',
+ },
+];
+
/** What we're working on. Not products, not prices — just intent. */
const PLANNED = [
{ icon: Shirt, label: 'Tees & hoodies' },
@@ -157,6 +180,74 @@ export default function AppShopPage() {
+ {/* ── Free to print ────────────────────────────────────────────────── */}
+
+
+
+
+ Free to print
+
+
+
+ no sats needed
+
+
+
+
+ {FREE_PRINT.map((item) => (
+
+
+
+
+ Free
+
+
+
+
+
{item.title}
+
+ {item.size}
+
+
+
+
+ {item.tip}
+
+
+
+
+
+
+
+ ))}
+
+
+
+ Print at 100% / “Actual size” so the size is
+ exact and the QR stays sharp — it points at skate-spots.org. These are vector files, so
+ they scale to any size without going fuzzy.
+
+
+
{/* ── How it'll work ───────────────────────────────────────────────── */}