Double Drum Press Risograph Printing & Zine Studio AI website prompt
A two-person riso studio in Rotterdam: six sections of two-colour ink, a draggable sticker wall and a working cart.
Now put it in WordPress
You have a site. Now make it yours.
An AI build hands you a front end and stops there. No CMS, no client login, no real ownership: you cannot hand it over, and you cannot change it without going back and re-prompting. UiChemy turns that build into native, editable WordPress you own, in the builder you already use.
Bolt to WordPressClaude to WordPressCodex to WordPressCursor to WordPressLovable to WordPressv0 to WordPressAI to WordPress
Get Started
More prompts
Showing 6 prompts
Frequently asked questions
What does the Double Drum Press Risograph Printing & Zine Studio prompt build?
A two-person riso studio in Rotterdam: six sections of two-colour ink, a draggable sticker wall and a working cart.
Which AI tools does the Double Drum Press Risograph Printing & Zine Studio prompt work with?
It's written for Claude. Paste the whole prompt into Claude and it builds the complete site for you, then bring the result into WordPress with UiChemy. This prompt also has a WordPress version: with the UiChemy MCP connected, Claude builds the page straight into WordPress.
How do I turn the result into a WordPress site?
After the AI builds your site, UiChemy Composer rebuilds it as native, editable WordPress in Elementor or Gutenberg (Bricks coming soon) — you own it and edit it in your builder. See AI to WordPress.
Is the Double Drum Press Risograph Printing & Zine Studio prompt free?
Yes — it's free to copy, no account needed.
Can I edit the website after it's built?
Yes. Once it's in WordPress with UiChemy, every section is native and editable — no re-prompting, no lock-in.
Copy prompt
Copy code
Coming soon
Copy SKILL.md
Coming soon
Double Drum Press Risograph Printing & Zine Studio · Section prompt
# MASTER PROMPT — Double Drum Press — Risograph Printing & Zine Studio
### Plain HTML, CSS and JavaScript. No WordPress, no page builder, no framework, no build step.
## ▶ HOW TO USE THIS PROMPT
**Paste this whole document into Claude. Nothing to attach.** It carries the complete source of the site plus the address of every image, so Claude can assemble a folder that owns its own assets and hand it back as a zip.
**INSTRUCTION TO CLAUDE — BUILD IT, DO NOT ASK.** Every decision is already made in this document. Do not stop to ask the user questions, do not offer options, and do not ask for confirmation before starting. Write the files, zip them, hand over the archive, then report what you built.
1. Create a folder named `double-drum` and write out every file in the appendix below **exactly as written** — do not reformat, rename, shorten, reorder or "improve" anything.
2. **Download the 15 images into `assets/`.** The code already points at `assets/…` — nothing in it references a remote host. Run this from inside the folder; it reads the ASSET MANIFEST at the end of this document:
```bash
mkdir -p assets
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-hero.jpg' -o 'assets/hero.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-j1.jpg' -o 'assets/j1.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-j2.jpg' -o 'assets/j2.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-printer.jpg' -o 'assets/printer.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s1.jpg' -o 'assets/s1.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s2.jpg' -o 'assets/s2.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s3.jpg' -o 'assets/s3.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s4.jpg' -o 'assets/s4.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-studio.jpg' -o 'assets/studio.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w1.jpg' -o 'assets/w1.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w2.jpg' -o 'assets/w2.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w3.jpg' -o 'assets/w3.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w4.jpg' -o 'assets/w4.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w5.jpg' -o 'assets/w5.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w6.jpg' -o 'assets/w6.jpg'
```
Then check every file arrived and none is a zero-byte or HTML error page:
```bash
ls assets | wc -l # expect 15 files
find assets -size -1k # expect no output
```
If any download fails, retry it — hosts occasionally drop a request. Do **not** fall back to hotlinking the remote URL: the whole point is a folder that works offline.
3. **Zip that folder and give the user the archive.** Its contents:
```
double-drum/
index.html
assets/
hero.jpg
j1.jpg
j2.jpg
printer.jpg
… 11 more
```
```bash
zip -r double-drum.zip double-drum -x '*.DS_Store'
```
Then hand over `double-drum.zip` as a file the user can download, not as text in the chat.
4. Before handing it over, serve the folder and confirm it runs:
```bash
cd double-drum && python3 -m http.server 8000 # then open http://localhost:8000
```
Check the page renders, the images load from `assets/` with the network throttled or offline, and scrolling drives the animations.
The finished folder needs no network for its images — they are in `assets/`; only the motion libraries load from a CDN.
Do not rebuild the design from the description below. The description is there so a person can understand and edit the code. The appendix is the deliverable.
---
## ▶ WHAT YOU GET
Double Drum Press is a two-person risograph print studio in Rotterdam. Small-run zines, posters and merch for artists, bands and people with loud ideas.
**1 code file plus an `assets/` folder holding 15 images, delivered as `double-drum.zip`.**
| File | What it is |
|---|---|
| `index.html` | a page, 158 KB |
| `assets/` | 15 images, downloaded at build time |
**Libraries, from a CDN, already referenced in the HTML:** `gsap.min.js`, `ScrollTrigger.min.js`, `lenis.min.js`. No npm install, no bundler, no framework.
**Sections (5):** `#dh-hero` · `#work` · `#studio` · `#shop` · `#journal`.
---
## ▶ WHERE THE MEDIA COMES FROM
All 15 files (15 images) live in `assets/` inside the folder, and every reference in the markup, the CSS and the script is a relative `assets/…` path. Once the folder is assembled it owns its own media: no CDN, no media library, no external image host. Open it on a laptop with the wifi off and it still renders.
They are fetched **once, at build time**, from the WordPress media library on `demo.uichemy.com` — the same `double-drum__` files the WordPress build of this page uses. That is only how they travel into the folder; nothing in the delivered code points back at that host. Replacing an image means dropping a new file into `assets/` under the same name.
The manifest at the end of this document lists every source URL against the filename it is saved as, so the download step is reproducible and checkable.
---
## ▶ WHAT THIS PROMPT DELIBERATELY DOES NOT CONTAIN
- No WordPress, no page builder, and none of the wrapper classes or compatibility CSS a builder needs. The WordPress build is a separate prompt.
- No framework, no TypeScript, no bundler, no `package.json`.
- No backend. Any form here validates and replies in place; it posts nowhere.
- No CMS. The content lives in the files.
- No external image host. The images ship inside the zip; nothing in the code calls out to a media library at runtime.
---
## ASSET MANIFEST — download these into `assets/`
Source is the media library on `demo.uichemy.com`. Save each one under the name in the second column; the code already refers to it by that name.
| Download from | Save as |
|---|---|
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-hero.jpg` | `assets/hero.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-j1.jpg` | `assets/j1.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-j2.jpg` | `assets/j2.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-printer.jpg` | `assets/printer.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s1.jpg` | `assets/s1.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s2.jpg` | `assets/s2.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s3.jpg` | `assets/s3.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-s4.jpg` | `assets/s4.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-studio.jpg` | `assets/studio.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w1.jpg` | `assets/w1.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w2.jpg` | `assets/w2.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w3.jpg` | `assets/w3.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w4.jpg` | `assets/w4.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w5.jpg` | `assets/w5.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/dd-w6.jpg` | `assets/w6.jpg` |
---
# APPENDIX · VERBATIM SOURCE
The complete source of every file. **Write them out exactly** — do not change a value, a comment, a class name, or the order of a declaration. Recreate this file layout:
```
index.html
```
## `index.html`
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>Double Drum Press — Risograph Printing & Zine Studio</title>
<meta name="description" content="Double Drum Press is a two-person risograph print studio in Rotterdam. Small-run zines, posters and merch for artists, bands and people with loud ideas.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=DM+Sans:opsz,[email protected],400;9..40,500;9..40,600;9..40,700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lenis.min.js"></script>
<style>
/* ---------- DOUBLE DRUM locked palette ---------- */
:root{
--bg:#FFFFFF; /* page: plain white stock */
--sun:#FFC62B; /* primary: sunflower drum (ink text only, never white text) */
--sun-deep:#F2AE00; /* primary hover */
--tomato:#E5402A; /* accent: tomato drum (white text OK) */
--tomato-deep:#C22E1A; /* accent hover */
--tomato-soft:#FCE1DC; /* accent tint */
--ink:#1F1C2B; /* text + hairlines — never a page/section/card background */
--mute:#5D5A6B; /* secondary text */
--grey:#F1F1F4; /* quiet cool panel */
--line:var(--ink); /* hairline colour: grid borders are 1px ink */
--display:'Anton', Impact, sans-serif;
--body:'DM Sans', system-ui, sans-serif;
--pixel:'Space Mono', ui-monospace, monospace;
--ease:cubic-bezier(.2,.7,.1,1);
--hair:1px solid var(--line);
}
*,*::before,*::after{box-sizing:border-box}
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto !important}
.lenis.lenis-stopped{overflow:hidden}
body{margin:0;font-synthesis-weight:none;background:var(--bg);color:var(--ink);font-family:var(--body);font-size:15px;line-height:1.5;overflow-x:hidden;-webkit-font-smoothing:antialiased}
img{display:block;max-width:100%}
a{color:inherit}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
:focus-visible{outline:2px solid var(--tomato);outline-offset:2px}
/* ---------- shared atoms: the hairline grid ---------- */
.page{border-inline:var(--hair);max-width:1600px;margin-inline:auto}
.cell{border-bottom:var(--hair);border-right:var(--hair)}
.pad{padding:clamp(14px,1.6vw,22px)}
.px{font-family:var(--pixel);font-size:11px;letter-spacing:.04em;text-transform:uppercase;line-height:1.3}
.big{font-family:var(--display);font-weight:400;text-transform:uppercase;line-height:.9;letter-spacing:0;margin:0}
.statement{font-family:var(--body);font-weight:500;font-size:clamp(24px,2.6vw,40px);line-height:1.08;letter-spacing:-.02em;margin:0}
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--tomato);vertical-align:middle}
.dot--sun{background:var(--sun);box-shadow:inset 0 0 0 1px var(--ink)}
.btn{display:inline-flex;align-items:center;gap:.5em;padding:10px 16px;border:var(--hair);border-radius:999px;background:var(--sun);color:var(--ink);
font-family:var(--pixel);font-size:12px;text-transform:uppercase;text-decoration:none;transition:background .2s var(--ease),color .2s var(--ease)}
.btn:hover{background:var(--tomato);color:#fff}
.btn--line{background:transparent}
.link{text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}
.link:hover{color:var(--tomato)}
.tick{overflow:hidden;white-space:nowrap;border-bottom:var(--hair)}
.kf-m{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.06em;margin-bottom:-.06em}
.kf-i{display:inline-block;will-change:transform}
.kf-w{display:inline-block;white-space:nowrap}
</style>
</head>
<body>
<script>
(function(){
gsap.registerPlugin(ScrollTrigger);
const reduce = matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
const DD = window.DD = { reduce, ease:{ out:'expo.out', soft:'power3.out', io:'power2.inOut', snap:'steps(1)' } };
if(!reduce && window.Lenis){
const lenis = DD.lenis = new Lenis({ lerp:.11 });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add(t => lenis.raf(t*1000));
gsap.ticker.lagSmoothing(0);
}
let rt; DD.refresh = () => { clearTimeout(rt); rt = setTimeout(() => ScrollTrigger.refresh(), 120); };
DD.later = fn => (window.requestIdleCallback || (f => setTimeout(f, 60)))(fn);
DD.velocity = () => DD.lenis ? DD.lenis.velocity : 0;
/* DD.split(el,'chars'|'words') -> inner spans in overflow masks (aria-label keeps the text readable) */
DD.split = function(el, mode){
mode = mode || 'words';
if(el._ddSplit) return el._ddSplit;
if(!el.hasAttribute('aria-label')) el.setAttribute('aria-label', el.textContent.replace(/\s+/g,' ').trim());
const out = [];
const mk = (t, p) => { const m = document.createElement('span'); m.className='kf-m'; m.setAttribute('aria-hidden','true');
const i = document.createElement('span'); i.className='kf-i'; i.textContent=t; m.appendChild(i); p.appendChild(m); out.push(i); };
const walk = (node, parent) => [...node.childNodes].forEach(n => {
if(n.nodeType === 3) n.textContent.split(/(\s+)/).forEach(p => {
if(!p) return;
if(/^\s+$/.test(p)) return parent.appendChild(document.createTextNode(' '));
if(mode === 'chars'){ const w = document.createElement('span'); w.className='kf-w'; w.setAttribute('aria-hidden','true'); [...p].forEach(c => mk(c, w)); parent.appendChild(w); }
else mk(p, parent);
});
else if(n.nodeType === 1){ const c = n.cloneNode(false); parent.appendChild(c); if(n.tagName !== 'BR') walk(n, c); }
});
const f = document.createDocumentFragment(); walk(el, f); el.textContent=''; el.appendChild(f);
return (el._ddSplit = out);
};
DD.reveal = function(el, o){
o = Object.assign({ mode:'words', start:'top 88%', delay:0 }, o);
const parts = DD.split(el, o.mode);
if(reduce) return null;
gsap.set(parts, { yPercent:110 });
return gsap.to(parts, { yPercent:0, duration:1, ease:DD.ease.out, delay:o.delay, stagger:o.mode === 'chars' ? .025 : .05,
scrollTrigger:{ trigger:el, start:o.start, once:true } });
};
/* DD.ticker(track, {speed, react}) -> seamless GSAP marquee. track must contain ONE copy of the content;
it is cloned until it covers 2x the viewport. react:true = scroll velocity changes speed/direction. */
DD.ticker = function(track, o){
o = Object.assign({ speed:60, react:true, dir:-1 }, o);
const unit = track.innerHTML; let x = 0, w = 0, boost = 0, dir = o.dir;
const fill = () => { track.innerHTML = unit; const one = track.scrollWidth || 1; let n = Math.ceil(innerWidth * 2 / one) + 1;
track.innerHTML = unit.repeat(Math.max(2, n)); w = one; };
fill(); addEventListener('resize', () => { clearTimeout(track._r); track._r = setTimeout(fill, 150); });
track.style.display = 'flex'; track.style.width = 'max-content'; track.style.willChange = 'transform';
[...track.children].forEach((c, i) => i && c.setAttribute('aria-hidden','true'));
if(reduce) return;
let on = true;
new IntersectionObserver(e => { on = e[0].isIntersecting; }).observe(track.parentElement || track);
gsap.ticker.add((t, dt) => {
if(!on) return;
if(o.react){ const v = DD.velocity(); if(Math.abs(v) > .3) dir = v > 0 ? o.dir : -o.dir; boost += (Math.min(Math.abs(v) * 6, 400) - boost) * .08; }
x += dir * (o.speed + boost) * Math.min(dt, 50) / 1000;
if(x <= -w) x += w; if(x > 0) x -= w;
track.style.transform = `translate3d(${x.toFixed(2)}px,0,0)`;
});
};
/* DD.cart: tiny shared cart state; sections listen for 'dd:cart' events */
DD.cart = { items:[], add(item){ this.items.push(item); dispatchEvent(new CustomEvent('dd:cart', { detail:this })); },
get total(){ return this.items.reduce((s, i) => s + i.price, 0); } };
addEventListener('load', DD.refresh);
})();
</script>
<div class="page">
<!-- ============ 01 · HERO (prefix dh-) : top ticker + sticky header + hero bento ============ -->
<div class="dh-top tick" id="top">
<div class="dh-top-track px">
<span class="dh-top-unit">
<span>New zine out: Drum Solo no.7 — <a class="dh-go" href="#shop">order your copy</a></span><span class="dh-sep" aria-hidden="true">✦</span>
<span><a class="dh-go" href="#studio">Workshop Thursday</a>: 2 seats left</span><span class="dh-sep" aria-hidden="true">✦</span>
<span>Now booking autumn print runs</span><span class="dh-sep" aria-hidden="true">✦</span>
</span>
</div>
</div>
<header class="dh-head" id="dh-header">
<a class="dh-cellh dh-logo dh-go" href="#top" aria-label="Double Drum Press, back to top">
<span class="dh-logo-box">
<svg class="dh-mark" viewBox="0 0 44 26" width="44" height="26" aria-hidden="true">
<circle class="dh-mk-a" cx="13" cy="13" r="11.5"/><circle class="dh-mk-b" cx="31" cy="13" r="11.5"/>
<circle class="dh-mk-c" cx="13" cy="13" r="2.2"/><circle class="dh-mk-c" cx="31" cy="13" r="2.2"/>
</svg>
<span class="dh-logo-word" aria-hidden="true"><span>DOUBLE/</span><span>DRUM</span></span>
</span>
</a>
<nav class="dh-cellh dh-nav" aria-label="Main">
<ul>
<li><a class="dh-go px" href="#work">Work</a></li>
<li><a class="dh-go px" href="#studio">Studio</a></li>
<li><a class="dh-go px" href="#shop">Shop</a></li>
<li><a class="dh-go px" href="#journal">Journal</a></li>
<li><a class="dh-go px" href="#contact">Contact</a></li>
</ul>
</nav>
<ul class="dh-cellh dh-status px" aria-label="Studio status">
<li><span>The Beast</span><i class="dot dh-blink"></i><span>printing</span></li>
<li><span>Ink</span><i class="dot dot--sun dh-blink dh-blink--2"></i><span>sun + tomato</span></li>
<li><span>Webshop</span><i class="dot dh-blink dh-blink--3"></i><span>online</span></li>
</ul>
<a class="dh-cellh dh-cart dh-go px" data-dd-cart href="#shop" aria-label="Cart, go to shop" aria-describedby="dh-cart-live">
<span class="dh-cart-flash" aria-hidden="true"></span>
<span class="dh-cart-in" aria-hidden="true">
<span class="dh-cart-lbl">Cart</span>
<span class="dh-cart-amt">€0.00</span>
<span class="dh-cart-cnt">(<span class="dh-cart-n"><span class="dh-cart-d">0</span></span>)</span>
</span>
</a>
<span class="dh-sr" id="dh-cart-live" aria-live="polite">Cart is empty</span>
<button class="dh-cellh dh-menu-btn px" type="button" aria-expanded="false" aria-controls="dh-menu">
<span class="dh-menu-ico" aria-hidden="true"><i></i><i></i></span><span class="dh-menu-txt">Menu</span>
</button>
<div class="dh-menu" id="dh-menu" hidden>
<nav aria-label="Mobile">
<ul class="dh-menu-list">
<li><a class="dh-go big" href="#work"><span class="px">01</span>Work</a></li>
<li><a class="dh-go big" href="#studio"><span class="px">02</span>Studio</a></li>
<li><a class="dh-go big" href="#shop"><span class="px">03</span>Shop</a></li>
<li><a class="dh-go big" href="#journal"><span class="px">04</span>Journal</a></li>
<li><a class="dh-go big" href="#contact"><span class="px">05</span>Contact</a></li>
</ul>
</nav>
<ul class="dh-menu-status px" aria-label="Studio status">
<li><span>The Beast</span><i class="dot dh-blink"></i><span>printing</span></li>
<li><span>Ink</span><i class="dot dot--sun dh-blink dh-blink--2"></i><span>sun + tomato</span></li>
<li><span>Webshop</span><i class="dot dh-blink dh-blink--3"></i><span>online</span></li>
</ul>
<p class="dh-menu-foot px">Maaskade 112, Rotterdam · <a class="link" href="mailto:[email protected]">[email protected]</a></p>
</div>
</header>
<section class="dh-hero" id="dh-hero" aria-labelledby="dh-title">
<div class="dh-bento">
<div class="dh-c dh-c-img cell">
<div class="dh-print" role="img" aria-label="Two-colour risograph print of a person in a pink jumper holding up a bright framed poster in front of their face, printed in sunflower and tomato ink">
<div class="dh-layer dh-l-sun"><img src="assets/hero.jpg" alt="" width="867" height="1300" fetchpriority="high"></div>
<div class="dh-layer dh-l-tom"><img src="assets/hero.jpg" alt="" width="867" height="1300"></div>
<div class="dh-feed" aria-hidden="true"></div>
<span class="dh-tag px" aria-hidden="true">Plate 01 — studio floor</span>
<span class="dh-reg px" aria-hidden="true">Reg <b class="dh-reg-v">0.0</b> mm</span>
</div>
<div class="dh-badge" aria-hidden="true">
<svg class="dh-badge-ring" viewBox="0 0 200 200">
<defs><path id="dh-bpath" d="M100,100 m-74,0 a74,74 0 1,1 148,0 a74,74 0 1,1 -148,0"/></defs>
<circle class="dh-badge-bg" cx="100" cy="100" r="98"/>
<circle class="dh-badge-in" cx="100" cy="100" r="56"/>
<text class="dh-badge-txt"><textPath href="#dh-bpath" textLength="462" lengthAdjust="spacing">PRINT LOUD • SINCE 2019 • PRINT LOUD • SINCE 2019 •</textPath></text>
</svg>
<svg class="dh-badge-mid" viewBox="0 0 200 200">
<circle class="dh-mk-a" cx="84" cy="100" r="26"/><circle class="dh-mk-b" cx="116" cy="100" r="26"/>
<circle class="dh-mk-c" cx="84" cy="100" r="5"/><circle class="dh-mk-c" cx="116" cy="100" r="5"/>
</svg>
</div>
</div>
<div class="dh-c dh-c-st cell pad dh-edge">
<p class="px dh-lbl dh-in">Double Drum Press · risograph studio · Rotterdam</p>
<h1 class="statement dh-title" id="dh-title">Small-run printing for loud ideas, two drums at a time.</h1>
</div>
<div class="dh-c dh-c-now cell pad dh-in">
<p class="px dh-lbl"><i class="dot dh-blink"></i> Now · 26.09</p>
<p class="dh-txt">Drum Solo no.7 just came off the guillotine: 48 pages, two drums, zero regrets. Kevin is sulking in blue while the Beast does the heavy lifting.</p>
<a class="link px dh-go dh-more" href="#contact">Book an autumn run →</a>
</div>
<div class="dh-c dh-c-shop cell pad dh-edge dh-in">
<p class="px dh-lbl">Shop › Latest</p>
<a class="dh-shop-link dh-go" href="#shop">
<span class="dh-mini" aria-hidden="true">
<span class="dh-layer dh-l-sun"><img src="assets/w6.jpg" alt="" width="867" height="1300" loading="lazy"></span>
<span class="dh-layer dh-l-tom"><img src="assets/w6.jpg" alt="" width="867" height="1300" loading="lazy"></span>
</span>
<span class="dh-shop-meta">
<span class="dh-shop-name">Drum Solo no.7 — €12</span>
<span class="px dh-mute">A5 · 48 pp · 2 inks · edition of 300</span>
<span class="px dh-shop-cta">View in shop →</span>
</span>
</a>
</div>
<div class="dh-c dh-c-bio cell pad dh-in">
<p class="px dh-lbl">Who's inking</p>
<p class="dh-txt">We're Mira and Sem: a printer and an illustrator sharing one studio on Noordereiland, two ink drums and a mild obsession with paper weights. Since 2019 we've printed for bands, bakers, festivals and anyone with something loud to say.</p>
<p class="px dh-mute dh-sig">Mira Okafor, printer · Sem de Wit, drawer of things</p>
</div>
<div class="dh-c dh-c-stat cell pad dh-edge dh-in">
<p class="px dh-lbl"><i class="dot dot--sun dh-blink dh-blink--2"></i> Live from the Beast</p>
<p class="dh-stat">
<span class="dh-sr">1,284,000 sheets printed</span>
<span class="dh-num-wrap big" aria-hidden="true"><span class="dh-num-ghost">1,284,000</span><span class="dh-num">1,284,000</span></span>
<span class="big dh-stat-lbl" aria-hidden="true">Sheets printed</span>
</p>
<p class="px dh-stat-sub">Since 2019 · and still counting</p>
</div>
<div class="dh-c dh-c-mq tick" aria-label="Print loud, print small, print twice">
<div class="dh-mq-track big" aria-hidden="true">
<span class="dh-mq-unit">
<span class="dh-w">Print</span><span class="dh-w dh-w--t">Loud</span><svg class="dh-star" viewBox="0 0 40 40"><path d="M20 1C21.2 13.5 26.5 18.8 39 20 26.5 21.2 21.2 26.5 20 39 18.8 26.5 13.5 21.2 1 20 13.5 18.8 18.8 13.5 20 1Z"/></svg>
<span class="dh-w">Print</span><span class="dh-w dh-w--t">Small</span><svg class="dh-star" viewBox="0 0 40 40"><path d="M20 1C21.2 13.5 26.5 18.8 39 20 26.5 21.2 21.2 26.5 20 39 18.8 26.5 13.5 21.2 1 20 13.5 18.8 18.8 13.5 20 1Z"/></svg>
<span class="dh-w">Print</span><span class="dh-w dh-w--t">Twice</span><svg class="dh-star" viewBox="0 0 40 40"><path d="M20 1C21.2 13.5 26.5 18.8 39 20 26.5 21.2 21.2 26.5 20 39 18.8 26.5 13.5 21.2 1 20 13.5 18.8 18.8 13.5 20 1Z"/></svg>
</span>
</div>
</div>
</div>
</section>
<style>
/* ---------- tokens local to the hero ---------- */
.dh-head,.dh-hero{--dh-h:64px}
.dh-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
/* ---------- top ticker ---------- */
.dh-top{background:var(--sun);color:var(--ink)}
.dh-top-track{display:flex;width:max-content;padding:9px 0 8px}
.dh-top-unit{display:flex;align-items:center;flex:none;white-space:nowrap}
.dh-top-unit a{text-decoration:underline;text-underline-offset:2px}
.dh-top-unit a:hover{color:var(--tomato-deep)}
.dh-sep{font-family:var(--body);font-size:12px;padding:0 22px}
/* ---------- sticky header ---------- */
.dh-head{position:sticky;top:0;z-index:50;background:var(--bg);border-bottom:var(--hair);height:var(--dh-h);
display:grid;grid-template-columns:auto 1fr auto auto;align-items:stretch}
.dh-cellh{display:flex;align-items:center;border-right:var(--hair);padding:0 20px;margin:0;min-width:0}
.dh-head > .dh-cart{border-right:0}
.dh-logo{padding:0 12px;text-decoration:none}
.dh-logo-box{display:flex;align-items:center;gap:9px;background:var(--sun);border:var(--hair);padding:6px 10px 6px 8px;height:44px;
transition:transform .25s var(--ease)}
.dh-logo:hover .dh-logo-box{transform:rotate(-2deg)}
.dh-logo-word{display:flex;flex-direction:column;font-family:var(--pixel);font-weight:700;font-size:13px;line-height:1;letter-spacing:.02em}
.dh-mark{flex:none;overflow:visible}
.dh-mk-a{fill:var(--tomato);stroke:var(--ink);stroke-width:1.2;mix-blend-mode:multiply}
.dh-mk-b{fill:var(--bg);fill-opacity:.35;stroke:var(--ink);stroke-width:1.2}
.dh-mk-c{fill:var(--ink)}
.dh-nav ul{display:flex;gap:clamp(14px,1.8vw,30px);list-style:none;margin:0;padding:0;flex-wrap:nowrap}
.dh-nav a{font-size:12px;text-decoration:underline;text-underline-offset:4px;text-decoration-thickness:1px;transition:color .15s}
.dh-nav a:hover{color:var(--tomato)}
.dh-status,.dh-menu-status{list-style:none;display:grid;grid-template-columns:auto 8px auto;column-gap:9px;row-gap:3px;align-content:center;font-size:10px;line-height:1.25}
.dh-status li,.dh-menu-status li{display:contents}
.dh-status li > span:last-child,.dh-menu-status li > span:last-child{color:var(--mute)}
.dh-status .dot,.dh-menu-status .dot{align-self:center;width:7px;height:7px}
.dh-blink{animation:dh-blink 1.1s steps(1,end) infinite}
.dh-blink--2{animation-duration:1.7s;animation-delay:-.4s}
.dh-blink--3{animation-duration:2.3s;animation-delay:-1.1s}
@keyframes dh-blink{0%{opacity:1}50%{opacity:.15}}
.dh-cart{position:relative;font-size:12px;text-decoration:none;overflow:hidden;min-width:178px;justify-content:center}
.dh-cart-flash{position:absolute;inset:0;background:var(--tomato);opacity:0;pointer-events:none}
.dh-cart-in{position:relative;display:flex;align-items:baseline;gap:8px;white-space:nowrap}
.dh-cart-lbl{text-decoration:underline;text-underline-offset:4px}
.dh-cart-amt{display:inline-block;min-width:5.6em;text-align:right}
.dh-cart-n{display:inline-grid;overflow:hidden;vertical-align:bottom;min-width:.7em;text-align:center}
.dh-cart-d{grid-area:1/1;display:block}
.dh-cart.is-hot{color:#fff}
.dh-cart:hover .dh-cart-lbl{color:var(--tomato)}
.dh-cart.is-hot:hover .dh-cart-lbl{color:#fff}
.dh-menu-btn{display:none;gap:10px;font-size:11px;border-right:0;border-left:var(--hair)}
.dh-menu-ico{display:grid;gap:5px;width:18px}
.dh-menu-ico i{display:block;height:2px;background:var(--ink);transition:transform .3s var(--ease)}
.dh-menu-btn[aria-expanded="true"] .dh-menu-ico i:first-child{transform:translateY(3.5px) rotate(45deg)}
.dh-menu-btn[aria-expanded="true"] .dh-menu-ico i:last-child{transform:translateY(-3.5px) rotate(-45deg)}
.dh-menu{position:fixed;left:0;right:0;bottom:0;top:var(--dh-h);z-index:49;background:var(--bg);overflow-y:auto;overscroll-behavior:contain;
border-top:var(--hair);display:flex;flex-direction:column}
.dh-menu[hidden]{display:none}
.dh-menu-list{list-style:none;margin:0;padding:0}
.dh-menu-list li{border-bottom:var(--hair);overflow:hidden}
.dh-menu-list a{display:flex;align-items:baseline;gap:14px;padding:14px 16px 10px;font-size:clamp(52px,15vw,84px);text-decoration:none}
.dh-menu-list a .px{font-size:11px;transform:translateY(-.4em)}
.dh-menu-list a:hover,.dh-menu-list a:focus-visible{color:var(--tomato)}
.dh-menu-status{padding:18px 16px;margin:0;border-bottom:var(--hair);font-size:12px;row-gap:8px;justify-content:start;background:var(--sun)}
.dh-menu-foot{padding:16px;margin:0;color:var(--mute)}
/* ---------- hero bento ---------- */
.dh-bento{display:grid;grid-template-columns:45fr 27.5fr 27.5fr;grid-template-rows:auto minmax(0,1fr) minmax(0,1fr) auto;
grid-template-areas:"img st st" "img now shop" "img bio stat" "mq mq mq";min-height:calc(100svh - var(--dh-h))}
.dh-c{position:relative;min-width:0}
.dh-edge{border-right:0}
.dh-c-img{grid-area:img;z-index:2}
.dh-c-st{grid-area:st;display:flex;flex-direction:column;justify-content:space-between;gap:18px;padding-bottom:clamp(18px,2vw,30px)}
.dh-c-now{grid-area:now}
.dh-c-shop{grid-area:shop}
.dh-c-bio{grid-area:bio}
.dh-c-stat{grid-area:stat;background:var(--tomato)}
.dh-c-mq{grid-area:mq}
.dh-c-now,.dh-c-bio,.dh-c-shop,.dh-c-stat{display:flex;flex-direction:column;gap:12px}
.dh-lbl{margin:0;display:flex;align-items:center;gap:8px}
.dh-mute{color:var(--mute)}
.dh-txt{margin:0;font-size:15px;line-height:1.45;max-width:36ch}
.dh-more{margin-top:auto;align-self:flex-start;font-size:11px}
.dh-title{font-size:clamp(28px,2.9vw,48px);max-width:17ch}
.dh-sig{margin:auto 0 0;font-size:10px}
/* the two-colour riso print */
.dh-print{position:absolute;inset:0;overflow:hidden;background:var(--bg)}
.dh-layer{position:absolute;inset:0;background:var(--bg);mix-blend-mode:multiply;isolation:isolate;will-change:transform}
.dh-layer img{width:100%;height:100%;object-fit:cover;object-position:50% 58%}
.dh-layer::after{content:"";position:absolute;inset:0;mix-blend-mode:screen;pointer-events:none}
.dh-l-sun img{filter:none}
.dh-l-sun::after{display:none}
.dh-l-tom{display:none}
.dh-l-tom::after{background:var(--tomato)}
.dh-print::after{content:"";position:absolute;inset:0;pointer-events:none;mix-blend-mode:multiply;opacity:.5;
background:radial-gradient(circle,rgba(31,28,43,.14) .8px,transparent 1.2px) 0 0/4px 4px}
.dh-feed{position:absolute;left:0;right:0;top:0;height:42%;z-index:2;pointer-events:none;transform:translateY(-101%);
background:radial-gradient(circle,rgba(229,64,42,.9) 1.6px,transparent 2.2px) 0 0/7px 7px,radial-gradient(circle,rgba(31,28,43,.55) 1px,transparent 1.5px) 3.5px 3.5px/7px 7px;
-webkit-mask-image:linear-gradient(to bottom,transparent,var(--ink) 55%,var(--ink) 92%,transparent);mask-image:linear-gradient(to bottom,transparent,var(--ink) 55%,var(--ink) 92%,transparent);
mix-blend-mode:multiply}
.dh-tag,.dh-reg{position:absolute;z-index:3;left:14px;background:var(--bg);border:var(--hair);padding:5px 8px 4px;font-size:10px}
.dh-tag{top:14px;background:var(--sun)}
.dh-reg{bottom:14px;display:none}
.dh-reg b{font-weight:400;display:inline-block;min-width:2.4em;text-align:right}
.dh-badge{position:absolute;right:-22px;bottom:-22px;width:clamp(112px,9vw,144px);aspect-ratio:1;z-index:6;pointer-events:none}
.dh-badge svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.dh-badge-bg{fill:var(--sun);stroke:var(--ink);stroke-width:1.4}
.dh-badge-in{fill:var(--bg);stroke:var(--ink);stroke-width:1.4}
.dh-badge-txt{font-family:var(--pixel);font-size:19px;fill:var(--ink);letter-spacing:.02em}
.dh-badge-mid .dh-mk-a,.dh-badge-mid .dh-mk-b{stroke-width:2.4}
/* shop mini print */
.dh-shop-link{display:grid;grid-template-columns:auto 1fr;gap:16px;align-items:end;text-decoration:none;margin-top:auto}
.dh-mini{position:relative;display:block;width:clamp(84px,7.2vw,118px);aspect-ratio:4/5;overflow:hidden;border:var(--hair);background:var(--bg)}
.dh-mini .dh-layer img{object-position:50% 50%}
.dh-shop-meta{display:flex;flex-direction:column;gap:6px}
.dh-shop-name{font-weight:600;font-size:16px;line-height:1.2;text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}
.dh-shop-link:hover .dh-shop-name,.dh-shop-link:hover .dh-shop-cta{color:var(--tomato)}
.dh-shop-cta{font-size:10px}
/* stat */
.dh-c-stat .dh-lbl,.dh-stat-sub{color:var(--ink)}
.dh-stat{margin:auto 0 0;display:flex;flex-direction:column;gap:4px}
.dh-num-wrap{position:relative;display:inline-block;align-self:flex-start;font-size:clamp(56px,5.6vw,100px);color:#fff}
.dh-num-ghost{visibility:hidden}
.dh-num{position:absolute;left:0;top:0}
.dh-stat-lbl{font-size:clamp(22px,1.9vw,34px);color:var(--ink)}
.dh-stat-sub{margin:0;font-size:10px}
/* marquee */
.dh-c-mq{border-bottom:var(--hair)}
.dh-mq-track{display:flex;width:max-content;font-size:clamp(84px,16vw,260px);padding:.06em 0 0}
.dh-mq-unit{display:flex;align-items:center;flex:none;white-space:nowrap}
.dh-w{color:var(--ink);padding-right:.18em}
.dh-w--t{color:var(--tomato)}
.dh-star{width:.52em;height:.52em;margin:0 .26em 0 .08em;flex:none;transform:translateY(-.06em)}
.dh-star path{fill:var(--sun);stroke:var(--ink);stroke-width:1.2;vector-effect:non-scaling-stroke}
/* ---------- tablet ---------- */
@media (max-width:1099px){
.dh-head{grid-template-columns:auto 1fr auto auto}
.dh-nav,.dh-status{display:none}
.dh-logo{border-right:0}
.dh-cart{grid-column:3;border-left:var(--hair);min-width:0;padding:0 16px}
.dh-head > .dh-cart{border-right:0}
.dh-menu-btn{display:flex;grid-column:4;padding:0 16px}
.dh-bento{grid-template-columns:1fr 1fr;grid-template-rows:auto;min-height:0;
grid-template-areas:"st st" "img now" "img shop" "bio stat" "mq mq"}
.dh-c-img{min-height:560px}
.dh-c-shop{border-right:0}
.dh-c-now{border-right:0}
.dh-c-bio{border-right:var(--hair)}
.dh-c-img{border-right:var(--hair)}
.dh-badge{right:-22px;bottom:-22px}
.dh-c-bio{padding-left:clamp(14px,1.6vw,22px)}
}
/* ---------- phone ---------- */
@media (max-width:699px){
.dh-head,.dh-hero{--dh-h:56px}
.dh-logo{padding:0 10px}
.dh-logo-box{height:38px;padding:5px 8px 5px 6px;gap:7px}
.dh-mark{width:36px;height:22px}
.dh-logo-word{font-size:11px}
.dh-cart{padding:0 12px;font-size:11px}
.dh-cart-lbl{display:none}
.dh-cart-amt{min-width:0}
.dh-menu-btn{padding:0 14px}
.dh-menu-txt{display:none}
.dh-bento{grid-template-columns:minmax(0,1fr);grid-template-areas:"st" "img" "now" "shop" "bio" "stat" "mq"}
.dh-c{border-right:0 !important}
.dh-c.pad{padding:18px 16px}
.dh-c-img{min-height:0;aspect-ratio:4/5}
.dh-badge{width:104px;right:16px;bottom:-44px}
.dh-c-now{padding-right:130px !important}
.dh-txt{max-width:none}
.dh-c-stat{padding-top:22px;padding-bottom:22px}
.dh-num-wrap{font-size:clamp(64px,19vw,96px)}
.dh-mq-track{font-size:26vw}
.dh-sep{padding:0 14px}
}
@media (min-width:700px) and (max-width:1099px){
.dh-c-now{padding-bottom:64px}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.dh-blink{animation:none}
.dh-feed{display:none}
.dh-logo-box,.dh-menu-ico i{transition:none}
}
</style>
<script>
(function(){
const DD = window.DD, R = DD.reduce;
const $ = (s, r) => (r || document).querySelector(s), $$ = (s, r) => [...(r || document).querySelectorAll(s)];
const header = $('#dh-header'), hero = $('#dh-hero');
/* ---------- tickers (DD clones the single unit) ---------- */
const topTrack = $('.dh-top-track'), mqTrack = $('.dh-mq-track');
DD.ticker(topTrack, { speed:42, react:false });
DD.ticker(mqTrack, { speed:95, react:true });
/* clones: hide from AT and keyboard (re-run after DD refills on resize) */
const tameClones = () => [topTrack, mqTrack].forEach(t => [...t.children].forEach((c, i) => { if(!i) return;
c.setAttribute('aria-hidden', 'true'); $$('a', c).forEach(a => a.tabIndex = -1); }));
tameClones(); addEventListener('resize', () => { clearTimeout(tameClones._t); tameClones._t = setTimeout(tameClones, 300); });
/* ---------- in-page links: offset by the sticky header ---------- */
const menu = $('#dh-menu'), mBtn = $('.dh-menu-btn');
const goTo = hash => {
if(hash === '#top'){ DD.lenis ? DD.lenis.scrollTo(0) : scrollTo({ top:0, behavior:R ? 'auto' : 'smooth' }); return true; }
const t = hash.length > 1 && document.querySelector(hash); if(!t) return false;
const off = header.offsetHeight;
if(DD.lenis) DD.lenis.scrollTo(t, { offset:-off, duration:1.2 });
else scrollTo({ top:t.getBoundingClientRect().top + scrollY - off, behavior:R ? 'auto' : 'smooth' });
return true;
};
$$('.dh-go').forEach(a => a.addEventListener('click', e => {
const h = a.getAttribute('href'); const wasOpen = !menu.hidden;
if(wasOpen) closeMenu(false);
if(goTo(h)){ e.preventDefault(); if(history.replaceState) history.replaceState(null, '', h === '#top' ? location.pathname : h); }
}));
/* ---------- mobile menu ---------- */
function openMenu(){
menu.style.top = Math.max(0, header.getBoundingClientRect().bottom) + 'px';
menu.hidden = false; mBtn.setAttribute('aria-expanded', 'true'); mBtn.setAttribute('aria-label', 'Close menu');
if(DD.lenis) DD.lenis.stop(); else document.documentElement.style.overflow = 'hidden';
if(!R){
gsap.fromTo(menu, { clipPath:'inset(0 0 100% 0)' }, { clipPath:'inset(0 0 0% 0)', duration:.7, ease:'expo.out' });
gsap.fromTo($$('.dh-menu-list a', menu), { yPercent:105 }, { yPercent:0, duration:.8, ease:'expo.out', stagger:.05, delay:.08 });
}
const first = $('.dh-menu-list a', menu); first && first.focus({ preventScroll:true });
}
function closeMenu(refocus){
if(menu.hidden) return;
menu.hidden = true; mBtn.setAttribute('aria-expanded', 'false'); mBtn.removeAttribute('aria-label');
if(DD.lenis) DD.lenis.start(); else document.documentElement.style.overflow = '';
if(refocus) mBtn.focus();
}
mBtn.addEventListener('click', () => menu.hidden ? openMenu() : closeMenu(true));
addEventListener('keydown', e => { if(e.key === 'Escape' && !menu.hidden) closeMenu(true); });
matchMedia('(min-width:1100px)').addEventListener('change', e => { if(e.matches) closeMenu(false); });
/* ---------- cart: listens to dd:cart ---------- */
const cart = $('.dh-cart'), amt = $('.dh-cart-amt', cart), nBox = $('.dh-cart-n', cart), flash = $('.dh-cart-flash', cart), live = $('#dh-cart-live');
const money = v => '€' + v.toFixed(2);
const shown = { v:0 }; let lastN = 0;
addEventListener('dd:cart', e => {
const c = (e && e.detail) || DD.cart, total = c.total, n = c.items.length;
live.textContent = n ? `Cart updated: ${n} item${n === 1 ? '' : 's'}, ${money(total)}` : 'Cart is empty';
if(R){ shown.v = total; amt.textContent = money(total); nBox.innerHTML = `<span class="dh-cart-d">${n}</span>`; lastN = n; return; }
gsap.to(shown, { v:total, duration:.9, ease:'expo.out', overwrite:true, onUpdate:() => { amt.textContent = money(shown.v); } });
if(n !== lastN){
const up = n > lastN, olds = [...nBox.children], nu = document.createElement('span');
nu.className = 'dh-cart-d'; nu.textContent = n; nBox.appendChild(nu);
gsap.fromTo(nu, { yPercent:up ? 110 : -110 }, { yPercent:0, duration:.55, ease:'expo.out' });
gsap.to(olds, { yPercent:up ? -110 : 110, duration:.55, ease:'expo.out', onComplete:() => olds.forEach(o => o.remove()) });
lastN = n;
}
cart.classList.add('is-hot');
gsap.fromTo(flash, { opacity:1 }, { opacity:0, duration:1, ease:'power2.in', overwrite:true });
clearTimeout(cart._t); cart._t = setTimeout(() => cart.classList.remove('is-hot'), 620);
gsap.fromTo($('.dh-cart-in', cart), { scale:1.1 }, { scale:1, duration:.6, ease:'back.out(2.5)', overwrite:true });
});
if(DD.cart && DD.cart.items.length) dispatchEvent(new CustomEvent('dd:cart', { detail:DD.cart }));
/* ---------- statement: word-by-word on load ---------- */
const words = DD.split($('#dh-title'), 'words');
/* ---------- the riso print ---------- */
const imgCell = $('.dh-c-img'), print = $('.dh-print', imgCell), sun = $('.dh-l-sun', print), tom = $('.dh-l-tom', print);
const feed = $('.dh-feed', print), regV = $('.dh-reg-v', print), badge = $('.dh-badge', imgCell), ring = $('.dh-badge-ring', badge);
const num = $('.dh-num'), TARGET = 1284000, fmt = v => Math.round(v).toLocaleString('en-US');
if(R){ feed.style.display = 'none'; return; } /* reduced motion: layers in register, static badge + tickers, final count */
/* intro timeline: layers fly in far out of register, then "print" into register; halftone feed wipes once */
gsap.set(sun, { xPercent:-46, yPercent:-7, rotation:-11, transformOrigin:'30% 60%' });
gsap.set(tom, { xPercent:46, yPercent:9, rotation:9, transformOrigin:'70% 40%' });
gsap.set(words, { yPercent:112 });
gsap.set(badge, { scale:0 });
gsap.set('.dh-in', { opacity:0, y:14 });
num.textContent = '0';
const tl = gsap.timeline({ delay:.12 });
tl.to(sun, { xPercent:0, yPercent:0, rotation:0, duration:1.55, ease:'expo.out' }, 0)
.to(tom, { xPercent:0, yPercent:0, rotation:0, duration:1.55, ease:'expo.out' }, .1)
.fromTo(feed, { yPercent:-101 }, { yPercent:245, duration:1.5, ease:'power2.inOut', onComplete:() => { feed.style.visibility = 'hidden'; } }, .2)
.to(words, { yPercent:0, duration:1.1, ease:'expo.out', stagger:.045 }, .3)
.to('.dh-in', { opacity:1, y:0, duration:.9, ease:'power3.out', stagger:.07 }, .45)
.to(badge, { scale:1, duration:.9, ease:'back.out(1.7)' }, 1.05);
const cnt = { v:0 };
tl.to(cnt, { v:TARGET, duration:2.4, ease:'expo.out', onUpdate:() => { num.textContent = fmt(cnt.v); } }, .5);
/* registration read-out follows the layers */
const readReg = () => {
const a = gsap.getProperty(sun, 'x') - gsap.getProperty(tom, 'x'), b = gsap.getProperty(sun, 'y') - gsap.getProperty(tom, 'y');
const pxOff = Math.hypot(a, b) + Math.abs(gsap.getProperty(sun, 'xPercent') - gsap.getProperty(tom, 'xPercent')) * print.offsetWidth / 100;
regV.textContent = (pxOff * .26).toFixed(1);
};
gsap.ticker.add(() => { if(heroOn) readReg(); });
/* live misregistration: pointer nudges layers in opposite directions */
const q = (el, p) => gsap.quickTo(el, p, { duration:.55, ease:'power3.out' });
const sx = q(sun, 'x'), sy = q(sun, 'y'), tx = q(tom, 'x'), ty = q(tom, 'y'), M = 6;
imgCell.addEventListener('pointermove', e => {
if(e.pointerType === 'touch') return;
const r = print.getBoundingClientRect(), nx = (e.clientX - r.left) / r.width - .5, ny = (e.clientY - r.top) / r.height - .5;
sx(nx * M); sy(ny * M); tx(-nx * M); ty(-ny * M);
});
imgCell.addEventListener('pointerleave', () => { sx(0); sy(0); tx(0); ty(0); });
/* shop mini print: slip out of register, snap back */
const mini = $('.dh-mini'), mS = $('.dh-l-sun', mini), mT = $('.dh-l-tom', mini);
const slip = () => gsap.timeline({ overwrite:true })
.to(mS, { x:-4, y:2, rotation:-1.2, duration:.18, ease:'power2.out' }, 0).to(mT, { x:4, y:-2, rotation:1.2, duration:.18, ease:'power2.out' }, 0)
.to([mS, mT], { x:0, y:0, rotation:0, duration:.7, ease:'elastic.out(1,.45)' }, .2);
$('.dh-shop-link').addEventListener('pointerenter', slip);
$('.dh-shop-link').addEventListener('focus', slip);
/* badge spin (speeds up with scroll velocity), paused offscreen */
const spin = gsap.to(ring, { rotation:360, duration:18, repeat:-1, ease:'none', transformOrigin:'50% 50%' });
let heroOn = true;
new IntersectionObserver(en => { heroOn = en[0].isIntersecting; heroOn ? spin.resume() : spin.pause(); }).observe(hero);
gsap.ticker.add(() => { if(heroOn) spin.timeScale(1 + Math.min(Math.abs(DD.velocity()) * .25, 6)); });
/* the Beast keeps printing: sheet counter ticks after the count-up */
tl.eventCallback('onComplete', () => {
let n = TARGET;
setInterval(() => { if(!heroOn || document.hidden) return; n += 1 + Math.floor(Math.random() * 3); num.textContent = fmt(n); }, 2400);
});
})();
</script>
<section id="work" class="dw" aria-labelledby="dw-title">
<h2 id="dw-title" class="dw-sr">Featured work</h2>
<!-- ===== top row: press log + intro ===== -->
<div class="dw-clip">
<div class="dw-top">
<div class="dw-log cell pad">
<div class="dw-log-head px">
<span><span class="dot dw-blink" aria-hidden="true"></span> Press log</span>
<span class="dw-mute">Week 39 / Maaskade 112</span>
</div>
<table class="dw-table px">
<caption class="dw-sr">What went through the presses this week</caption>
<thead><tr><th scope="col">Type</th><th scope="col">Item</th></tr></thead>
<tbody>
<tr><td data-dw-t>Job</td><td><a class="link" href="#dw-heatwave" data-dw-t>Heatwave posters</a></td></tr>
<tr><td data-dw-t>Paper</td><td><a class="link" href="#studio" data-dw-t>20,000 sheets of Munken</a></td></tr>
<tr><td data-dw-t>Zine</td><td><a class="link" href="#shop" data-dw-t>Drum Solo no.7</a></td></tr>
<tr><td data-dw-t>Fair</td><td><a class="link" href="#dw-softriot" data-dw-t>Soft Riot 2026</a></td></tr>
<tr><td data-dw-t>Break</td><td><a class="link" href="#studio" data-dw-t>Kevin jammed again</a><span class="dw-cur" aria-hidden="true"></span></td></tr>
</tbody>
</table>
<p class="dw-log-foot px dw-mute">Ink used: 2 drums / Coffee used: yes</p>
</div>
<div class="dw-intro cell pad">
<div class="dw-intro-head">
<p class="px dw-label"><span class="dw-on">Featured work</span> / <a class="dw-all" href="#dw-grid">All work</a></p>
<span class="dw-pill px"><span class="dot dw-blink" aria-hidden="true"></span> Available for runs</span>
</div>
<p class="statement dw-statement">Posters, zines and sleeves for people who’d rather be heard than scrolled past. Two drums, two colours, zero chill.</p>
<div class="dw-intro-foot">
<p class="px dw-mute">06 projects / 2 inks / 1 grumpy backup machine</p>
<a class="link dw-mail" href="mailto:[email protected]">[email protected]</a>
</div>
</div>
</div>
</div>
<!-- ===== client ticker ===== -->
<div class="tick dw-tick" aria-label="Clients">
<div class="dw-track"><span class="dw-unit">
<span class="big dw-cl">Northsea Jazz Club</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
<span class="big dw-cl">Maas Radio</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
<span class="big dw-cl">Tiny Fires Records</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
<span class="big dw-cl">Lowlands Bakery</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
<span class="big dw-cl">Kade Film Night</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
<span class="big dw-cl">Soft Riot Zine Fair</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
<span class="big dw-cl">Hoek Bikes</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
<span class="big dw-cl">Studio Balsa</span><svg class="dw-drum" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><circle cx="12" cy="12" r="4.5" fill="none" stroke="#fff" stroke-width="2"/></svg>
</span></div>
</div>
<!-- ===== featured work grid ===== -->
<div class="dw-clip">
<div class="dw-grid" id="dw-grid">
<button class="dw-card cell dw-c1" id="dw-heatwave" data-dw-i="0" aria-haspopup="dialog">
<span class="dw-print"><span class="dw-inner">
<span class="dw-ly dw-ly--sun"><img src="assets/w2.jpg" alt="A wall covered in bright gig and radio posters" loading="lazy" style="object-position:50% 40%"></span>
<span class="dw-ly dw-ly--tom" aria-hidden="true"><img src="assets/w2.jpg" alt="" loading="lazy" style="object-position:50% 40%"></span>
</span></span>
<span class="dw-cap"><span class="px dw-name"><b>Heatwave</b> / Maas Radio</span><span class="dw-tag px">Poster series</span></span>
</button>
<button class="dw-card cell dw-c2" id="dw-tinyfires" data-dw-i="1" aria-haspopup="dialog">
<span class="dw-print"><span class="dw-inner">
<span class="dw-ly dw-ly--sun"><img src="assets/w6.jpg" alt="A tall stack of printed zines and magazines" loading="lazy" style="object-position:50% 45%"></span>
<span class="dw-ly dw-ly--tom" aria-hidden="true"><img src="assets/w6.jpg" alt="" loading="lazy" style="object-position:50% 45%"></span>
</span></span>
<span class="dw-cap"><span class="px dw-name"><b>Tiny Fires Vol. 3</b> / Tiny Fires</span><span class="dw-tag px">Record sleeve</span></span>
</button>
<button class="dw-card cell dw-c3" id="dw-softriot" data-dw-i="2" aria-haspopup="dialog">
<span class="dw-print"><span class="dw-inner">
<span class="dw-ly dw-ly--sun"><img src="assets/w3.jpg" alt="An open printed booklet with small line drawings" loading="lazy" style="object-position:50% 35%"></span>
<span class="dw-ly dw-ly--tom" aria-hidden="true"><img src="assets/w3.jpg" alt="" loading="lazy" style="object-position:50% 35%"></span>
</span></span>
<span class="dw-cap"><span class="px dw-name"><b>Soft Riot 2026</b> / Soft Riot Zine Fair</span><span class="dw-tag px">Identity</span></span>
</button>
<button class="dw-card cell dw-c4" id="dw-bakeloud" data-dw-i="3" aria-haspopup="dialog">
<span class="dw-print"><span class="dw-inner">
<span class="dw-ly dw-ly--sun"><img src="assets/w1.jpg" alt="An ink roller resting on a freshly printed blue sheet" loading="lazy" style="object-position:50% 30%"></span>
<span class="dw-ly dw-ly--tom" aria-hidden="true"><img src="assets/w1.jpg" alt="" loading="lazy" style="object-position:50% 30%"></span>
</span></span>
<span class="dw-cap"><span class="px dw-name"><b>Bake Loud</b> / Lowlands Bakery</span><span class="dw-tag px">Packaging</span></span>
</button>
<button class="dw-card cell dw-c5" id="dw-kade" data-dw-i="4" aria-haspopup="dialog">
<span class="dw-print"><span class="dw-inner">
<span class="dw-ly dw-ly--sun"><img src="assets/w4.jpg" alt="Four framed film posters hung above a yellow sofa" loading="lazy" style="object-position:50% 40%"></span>
<span class="dw-ly dw-ly--tom" aria-hidden="true"><img src="assets/w4.jpg" alt="" loading="lazy" style="object-position:50% 40%"></span>
</span></span>
<span class="dw-cap"><span class="px dw-name"><b>Kade Film Night</b> / Kade</span><span class="dw-tag px">Monthly posters</span></span>
</button>
<button class="dw-card cell dw-c6" id="dw-twowheels" data-dw-i="5" aria-haspopup="dialog">
<span class="dw-print"><span class="dw-inner">
<span class="dw-ly dw-ly--sun"><img src="assets/w5.jpg" alt="Hands inking and printing sheets at a long work table" loading="lazy" style="object-position:50% 50%"></span>
<span class="dw-ly dw-ly--tom" aria-hidden="true"><img src="assets/w5.jpg" alt="" loading="lazy" style="object-position:50% 50%"></span>
</span></span>
<span class="dw-cap"><span class="px dw-name"><b>Two Wheels Good</b> / Hoek Bikes</span><span class="dw-tag px">Zine</span></span>
</button>
</div>
</div>
<div class="dw-look px" aria-hidden="true">Look</div>
<!-- ===== project drawer ===== -->
<div class="dw-modal" hidden>
<div class="dw-scrim" data-dw-close></div>
<div class="dw-drawer" role="dialog" aria-modal="true" aria-labelledby="dw-m-title" data-lenis-prevent>
<div class="dw-m-bar">
<p class="px"><span class="dot dw-blink" aria-hidden="true"></span> Project <span class="dw-m-n">01</span> / 06</p>
<div class="dw-m-btns">
<button class="dw-m-btn px dw-m-next" type="button">Next →</button>
<button class="dw-m-btn px dw-m-close" type="button" data-dw-close aria-label="Close project">Close ×</button>
</div>
</div>
<div class="dw-m-print dw-print"><span class="dw-inner">
<span class="dw-ly dw-ly--sun"><img src="assets/w2.jpg" alt=""></span>
<span class="dw-ly dw-ly--tom" aria-hidden="true"><img src="assets/w2.jpg" alt=""></span>
</span></div>
<div class="dw-m-body">
<p class="px dw-m-meta"><span class="dw-m-client">Maas Radio</span><span class="dw-tag px dw-m-tag">Poster series</span></p>
<h3 class="big dw-m-title" id="dw-m-title">Heatwave</h3>
<p class="dw-m-blurb"></p>
<dl class="dw-specs px">
<div><dt>Colours</dt><dd>Sunflower + Tomato</dd></div>
<div><dt>Paper</dt><dd>120g recycled white</dd></div>
<div><dt>Run</dt><dd class="dw-m-run">300</dd></div>
<div><dt>Size</dt><dd class="dw-m-size">A2</dd></div>
</dl>
<a class="btn dw-m-cta" href="mailto:[email protected]?subject=A%20run%20like%20this%20one">Print me something like this</a>
</div>
</div>
</div>
</section>
<style>
.dw{scroll-margin-top:110px;position:relative}
.dw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;margin:-1px;padding:0;border:0}
.dw-mute{color:var(--mute)}
/* hairline grids overhang by 1px so the last column's right line hides under the page border */
.dw-clip{overflow:hidden}
.dw-top,.dw-grid{margin-right:-1px}
@keyframes dw-blink{50%{opacity:0}}
.dw-blink{animation:dw-blink 1.1s steps(1) infinite}
/* ---------- top row ---------- */
.dw-top{display:grid;grid-template-columns:5fr 7fr}
.dw-log{background:var(--grey);display:flex;flex-direction:column;gap:18px}
.dw-log-head{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
.dw-log-head .dot{margin-right:6px;margin-top:-2px}
.dw-table{width:100%;border-collapse:collapse;table-layout:fixed;font-size:13px}
.dw-table th{font-weight:400;color:var(--mute);text-align:left;padding:0 0 8px;border-bottom:var(--hair)}
.dw-table th:last-child,.dw-table td:last-child{text-align:right}
.dw-table th:first-child{width:34%}
.dw-table td{padding:11px 0;border-bottom:1px solid rgba(31,28,43,.18);white-space:nowrap;overflow:hidden;text-overflow:clip;height:44px}
.dw-table tr:last-child td{border-bottom:0}
.dw-table a{text-decoration-color:var(--tomato)}
.dw-cur{display:inline-block;width:.62em;height:1.05em;background:var(--tomato);vertical-align:-.2em;margin-left:4px;animation:dw-blink .8s steps(1) infinite}
.dw-log-foot{margin:auto 0 0}
.dw-intro{display:flex;flex-direction:column;gap:clamp(24px,3vw,48px);justify-content:space-between}
.dw-intro-head{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.dw-label{margin:0}
.dw-on{background:var(--sun);padding:2px 4px;box-shadow:inset 0 0 0 1px var(--ink)}
.dw-all{color:var(--mute);text-decoration:none}
.dw-all:hover{color:var(--tomato)}
.dw-pill{display:inline-flex;align-items:center;gap:8px;background:var(--sun);border:var(--hair);border-radius:999px;padding:6px 12px 5px}
.dw-statement{font-size:clamp(28px,3.3vw,52px);max-width:20ch}
.dw-intro-foot{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap}
.dw-intro-foot p{margin:0}
.dw-mail{font-weight:600;font-size:17px}
/* ---------- client ticker ---------- */
.dw-tick{padding:clamp(10px,1.4vw,20px) 0}
.dw-unit{display:inline-flex;align-items:center;flex:none}
.dw-cl{font-size:clamp(54px,8.6vw,136px);padding:0 .22em;white-space:nowrap}
.dw-drum{width:clamp(18px,1.8vw,28px);height:auto;color:var(--tomato);flex:none}
/* ---------- riso print (shared by grid + drawer) ---------- */
.dw-print{position:relative;display:block;overflow:hidden;background:#fff;isolation:isolate}
.dw-inner{position:absolute;inset:0;display:block}
.dw-ly{position:absolute;inset:-20px;display:block;mix-blend-mode:multiply;background:#fff}
.dw-ly img{width:100%;height:100%;object-fit:cover}
.dw-ly::after{content:"";position:absolute;inset:0;mix-blend-mode:screen}
.dw-ly--sun img{filter:none}
.dw-ly--sun::after{display:none}
.dw-ly--tom{display:none}
.dw-ly--tom::after{background:var(--tomato)}
.dw-ly--sun{transform:translate(-3px,-2px)}
.dw-ly--tom{transform:translate(3px,2px)}
/* halftone grain on top */
.dw-print::after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;mix-blend-mode:multiply;opacity:.55;
background:radial-gradient(circle at 50% 50%,rgba(31,28,43,.16) 0 .8px,transparent 1.2px) 0 0/4px 4px}
/* ---------- featured grid ---------- */
.dw-grid{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(8,clamp(84px,7vw,118px))}
.dw-card{display:flex;flex-direction:column;text-align:left;padding:0;min-width:0;background:#fff;scroll-margin-top:110px}
.dw-card .dw-print{flex:1 1 auto;min-height:0}
.dw-c1{grid-column:1;grid-row:1/6}
.dw-c2{grid-column:2;grid-row:1/4}
.dw-c3{grid-column:3;grid-row:1/5}
.dw-c4{grid-column:1;grid-row:6/9}
.dw-c5{grid-column:2;grid-row:4/9}
.dw-c6{grid-column:3;grid-row:5/9}
.dw-cap{display:flex;justify-content:space-between;align-items:center;gap:10px;border-top:var(--hair);padding:10px clamp(14px,1.6vw,22px);min-height:46px;background:#fff;transition:background .25s var(--ease)}
.dw-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dw-name b{font-weight:700}
.dw-tag{flex:none;border:var(--hair);border-radius:999px;padding:4px 9px 3px;background:#fff;font-size:10px}
.dw-card:hover .dw-cap,.dw-card:focus-visible .dw-cap{background:var(--sun)}
.dw-card:hover .dw-tag,.dw-card:focus-visible .dw-tag{background:var(--tomato);color:#fff}
.dw-card:focus-visible{outline-offset:-4px}
.dw--look .dw-card{cursor:none}
/* LOOK sticker cursor */
.dw-look{position:fixed;left:0;top:0;z-index:70;pointer-events:none;display:grid;place-items:center;width:62px;height:62px;border-radius:50%;
background:var(--tomato);color:#fff;border:var(--hair);font-size:13px;letter-spacing:.08em;transform:scale(0);opacity:0}
/* ---------- drawer ---------- */
.dw-modal{position:fixed;inset:0;z-index:80}
.dw-modal[hidden]{display:none}
.dw-scrim{position:absolute;inset:0;background:rgba(255,198,43,.42)}
.dw-drawer{position:absolute;top:0;right:0;bottom:0;width:min(620px,100%);background:#fff;border-left:var(--hair);overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column}
.dw-m-bar{position:sticky;top:0;z-index:3;display:flex;justify-content:space-between;align-items:center;gap:10px;background:#fff;border-bottom:var(--hair);padding:12px clamp(16px,2vw,24px)}
.dw-m-bar p{margin:0}
.dw-m-bar .dot{margin-right:6px;margin-top:-2px}
.dw-m-btns{display:flex;gap:8px}
.dw-m-btn{border:var(--hair);border-radius:999px;padding:7px 12px 6px;background:#fff;transition:background .2s var(--ease),color .2s var(--ease)}
.dw-m-btn:hover{background:var(--sun)}
.dw-m-close:hover{background:var(--tomato);color:#fff}
.dw-m-print{flex:none;aspect-ratio:5/4;border-bottom:var(--hair)}
.dw-m-body{padding:clamp(16px,2.4vw,28px);display:flex;flex-direction:column;gap:16px}
.dw-m-meta{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:0}
.dw-m-title{font-size:clamp(48px,7vw,88px)}
.dw-m-blurb{margin:0;font-size:17px;line-height:1.5;max-width:46ch}
.dw-specs{margin:0;border-top:var(--hair);font-size:12px}
.dw-specs div{display:flex;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:var(--hair)}
.dw-specs dt{color:var(--mute)}
.dw-specs dd{margin:0;text-align:right}
.dw-m-cta{align-self:flex-start;margin-top:6px}
/* ---------- responsive ---------- */
@media (max-width:1000px){
.dw-top{grid-template-columns:1fr}
.dw-grid{grid-template-columns:repeat(2,1fr);grid-template-rows:none}
.dw-grid .dw-card{grid-column:auto;grid-row:auto}
.dw-card .dw-print{flex:none;aspect-ratio:4/5}
.dw-c2 .dw-print,.dw-c3 .dw-print{aspect-ratio:1/1}
}
@media (max-width:640px){
.dw .pad,.dw-cap{padding-left:16px;padding-right:16px}
.dw-grid{grid-template-columns:1fr}
.dw-card .dw-print,.dw-c2 .dw-print,.dw-c3 .dw-print{aspect-ratio:4/3}
.dw-c1 .dw-print,.dw-c5 .dw-print{aspect-ratio:4/5}
.dw-table{font-size:11px}
.dw-table th:first-child{width:28%}
.dw-m-bar{padding:10px 16px}
.dw-m-body{padding:16px}
.dw-cap{flex-wrap:wrap}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.dw-blink,.dw-cur{animation:none}
.dw-cap,.dw-m-btn{transition:none}
}
</style>
<script>
DD.later(() => {
const root = document.getElementById('work');
if(!root) return;
const reduce = DD.reduce;
const fine = matchMedia('(hover: hover) and (pointer: fine)').matches;
/* ---------- press log: scramble-type rows one by one ---------- */
const rows = [...root.querySelectorAll('.dw-table tbody tr')];
const cur = root.querySelector('.dw-cur');
const GLYPHS = '#%&*+=/<>?!$@01';
if(!reduce){
const cells = rows.map(r => [...r.querySelectorAll('[data-dw-t]')]);
cells.flat().forEach(el => { el.dataset.full = el.textContent; el.textContent = ' '; });
cur.style.visibility = 'hidden';
const type = (el, done) => {
const full = el.dataset.full, len = full.length, dur = Math.min(900, 160 + len * 34);
const place = el.closest('td'); place.appendChild(cur); cur.style.visibility = 'visible';
let t0 = null;
const step = t => {
if(t0 === null) t0 = t;
const p = Math.min(1, (t - t0) / dur), n = Math.floor(p * len);
let s = full.slice(0, n);
for(let k = n; k < Math.min(len, n + 3); k++) s += full[k] === ' ' ? ' ' : GLYPHS[(Math.random() * GLYPHS.length) | 0];
el.textContent = s || ' ';
if(p < 1) requestAnimationFrame(step); else { el.textContent = full; done && done(); }
};
requestAnimationFrame(step);
};
const queue = cells.flat();
const run = i => { if(i >= queue.length) return; type(queue[i], () => setTimeout(() => run(i + 1), i % 2 ? 180 : 60)); };
ScrollTrigger.create({ trigger: root.querySelector('.dw-log'), start: 'top 78%', once: true, onEnter: () => run(0) });
}
/* ---------- statement reveal ---------- */
DD.reveal(root.querySelector('.dw-statement'), { mode: 'words' });
/* ---------- client ticker ---------- */
DD.ticker(root.querySelector('.dw-track'), { speed: 70, react: true });
/* ---------- riso prints ---------- */
const REST = { sun: { x: -3, y: -2 }, tom: { x: 3, y: 2 } };
const cards = [...root.querySelectorAll('.dw-card')];
cards.forEach(card => {
const inner = card.querySelector('.dw-inner');
const sun = card.querySelector('.dw-ly--sun'), tom = card.querySelector('.dw-ly--tom');
if(reduce) return;
gsap.set(inner, { clipPath: 'inset(0% 0% 100% 0%)' });
gsap.set(sun, { x: -16, y: -10 }); gsap.set(tom, { x: 16, y: 10 });
gsap.timeline({ scrollTrigger: { trigger: card, start: 'top 85%', once: true } })
.to(inner, { clipPath: 'inset(0% 0% 0% 0%)', duration: .75, ease: 'steps(9)' })
.to(sun, { x: REST.sun.x, y: REST.sun.y, duration: 1.1, ease: DD.ease.out }, .55)
.to(tom, { x: REST.tom.x, y: REST.tom.y, duration: 1.1, ease: DD.ease.out }, .62);
const over = () => {
gsap.to(inner, { scale: 1.04, duration: .6, ease: DD.ease.soft, overwrite: 'auto' });
gsap.to(sun, { x: -9, y: -5, duration: .35, ease: DD.ease.soft, overwrite: 'auto' });
gsap.to(tom, { x: 9, y: 5, duration: .35, ease: DD.ease.soft, overwrite: 'auto' });
};
const out = () => {
gsap.to(inner, { scale: 1, duration: .6, ease: DD.ease.soft, overwrite: 'auto' });
gsap.to(sun, { x: REST.sun.x, y: REST.sun.y, duration: .7, ease: 'back.out(2.4)', overwrite: 'auto' });
gsap.to(tom, { x: REST.tom.x, y: REST.tom.y, duration: .7, ease: 'back.out(2.4)', overwrite: 'auto' });
};
card.addEventListener('mouseenter', over); card.addEventListener('mouseleave', out);
card.addEventListener('focus', () => card.matches(':focus-visible') && over()); card.addEventListener('blur', out);
});
/* ---------- LOOK sticker cursor (mouse only) ---------- */
const look = root.querySelector('.dw-look');
if(fine && !reduce){
root.classList.add('dw--look');
const qx = gsap.quickTo(look, 'x', { duration: .35, ease: 'power3' });
const qy = gsap.quickTo(look, 'y', { duration: .35, ease: 'power3' });
gsap.set(look, { xPercent: -50, yPercent: -50, rotate: -10 });
let shown = false;
const show = on => { if(on === shown) return; shown = on;
gsap.to(look, { scale: on ? 1 : 0, opacity: on ? 1 : 0, rotate: on ? -10 : -40, duration: on ? .45 : .25, ease: on ? 'back.out(2)' : 'power2.in', overwrite: 'auto' }); };
cards.forEach(c => {
c.addEventListener('pointerenter', e => { if(e.pointerType !== 'mouse') return; if(!shown){ gsap.set(look, { x: e.clientX, y: e.clientY }); } show(true); });
c.addEventListener('pointerleave', e => { if(e.pointerType === 'mouse') show(false); });
c.addEventListener('pointermove', e => { if(e.pointerType !== 'mouse') return; qx(e.clientX); qy(e.clientY); });
});
addEventListener('scroll', () => { if(shown && !cards.some(c => c.matches(':hover'))) show(false); }, { passive: true });
root._dwHideLook = () => show(false);
}
/* ---------- project drawer ---------- */
const P = [
{ t: 'Heatwave', c: 'Maas Radio', tag: 'Poster series', img: 'w2', pos: '50% 40%', run: '300', size: 'A2',
b: 'Six posters for Maas Radio’s summer late-night shows, each one hotter than the last — we pushed so much tomato through the Beast the drum needed a lie-down. They were pasted up across Noordereiland and peeled off the walls by fans within a week, which we choose to read as a review.' },
{ t: 'Tiny Fires Vol. 3', c: 'Tiny Fires Records', tag: 'Record sleeve', img: 'w6', pos: '50% 45%', run: '500', size: '31 × 31 cm + A5 zine',
b: 'A sleeve and a 24-page lyric zine for the label’s third compilation, printed as a deliberately mismatched pair. Every copy lands a little off-register in its own way, so no two fans own quite the same record.' },
{ t: 'Soft Riot 2026', c: 'Soft Riot Zine Fair', tag: 'Identity', img: 'w3', pos: '50% 35%', run: '1,200', size: 'A2 folded to A6',
b: 'The whole fair in two drums: wristbands, wayfinding and a fold-out programme that doubles as a floor plan. One chunky typeface and a lot of sunflower, because the room is loud enough already.' },
{ t: 'Bake Loud', c: 'Lowlands Bakery', tag: 'Packaging', img: 'w1', pos: '50% 30%', run: '2,000', size: 'A3 sheets, die-cut',
b: 'Bread wraps, bag stickers and a stamp card for Lowlands Bakery that smell faintly of soy ink until the croissants win. Sunflower for the crust, tomato for the jam, nothing else needed.' },
{ t: 'Kade Film Night', c: 'Kade Film Night', tag: 'Monthly posters', img: 'w4', pos: '50% 40%', run: '150 a month', size: 'A2',
b: 'A fresh poster every month for the harbour screenings, drawn the week of the film and printed the night before. People now queue for them longer than they queue for popcorn.' },
{ t: 'Two Wheels Good', c: 'Hoek Bikes', tag: 'Zine', img: 'w5', pos: '50% 50%', run: '400', size: 'A6, hand-stapled',
b: 'A pocket zine for Hoek Bikes on fixing a flat, riding in sideways rain and ranking Rotterdam’s bridges. Stapled by hand and sized to live in a saddlebag.' }
];
const modal = root.querySelector('.dw-modal');
const drawer = modal.querySelector('.dw-drawer'), scrim = modal.querySelector('.dw-scrim');
const mPrint = modal.querySelector('.dw-m-print');
const mImgs = mPrint.querySelectorAll('img');
const mSun = mPrint.querySelector('.dw-ly--sun'), mTom = mPrint.querySelector('.dw-ly--tom');
const q = s => modal.querySelector(s);
let idx = 0, lastFocus = null, open = false;
const fill = i => {
idx = (i + P.length) % P.length; const p = P[idx];
mImgs.forEach((im, k) => { im.src = `assets/${p.img}.jpg`; im.style.objectPosition = p.pos; im.alt = k ? '' : `${p.t} for ${p.c}, shown as a two-colour riso print`; });
q('.dw-m-n').textContent = String(idx + 1).padStart(2, '0');
q('.dw-m-client').textContent = p.c; q('.dw-m-tag').textContent = p.tag;
q('.dw-m-title').textContent = p.t; q('.dw-m-blurb').textContent = p.b;
q('.dw-m-run').textContent = p.run; q('.dw-m-size').textContent = p.size;
drawer.scrollTop = 0;
if(!reduce){
gsap.fromTo(mSun, { x: -18, y: -12 }, { x: REST.sun.x, y: REST.sun.y, duration: 1, ease: DD.ease.out, delay: .15, overwrite: true });
gsap.fromTo(mTom, { x: 18, y: 12 }, { x: REST.tom.x, y: REST.tom.y, duration: 1, ease: DD.ease.out, delay: .2, overwrite: true });
}
};
const focusables = () => [...drawer.querySelectorAll('a[href],button:not([disabled]),[tabindex]:not([tabindex="-1"])')].filter(el => el.offsetParent !== null);
const openM = i => {
if(open) return fill(i);
open = true; lastFocus = document.activeElement;
root._dwHideLook && root._dwHideLook();
fill(i); modal.hidden = false;
DD.lenis && DD.lenis.stop();
if(!reduce){
gsap.fromTo(scrim, { opacity: 0 }, { opacity: 1, duration: .3, ease: 'power1.out' });
gsap.fromTo(drawer, { xPercent: 100 }, { xPercent: 0, duration: .6, ease: DD.ease.out });
}
q('.dw-m-close').focus({ preventScroll: true });
};
const closeM = () => {
if(!open) return; open = false;
const done = () => { modal.hidden = true; DD.lenis && DD.lenis.start(); lastFocus && lastFocus.focus({ preventScroll: true }); };
if(reduce) return done();
gsap.to(scrim, { opacity: 0, duration: .3, ease: 'power1.in' });
gsap.to(drawer, { xPercent: 100, duration: .4, ease: 'power3.in', onComplete: done });
};
cards.forEach(c => c.addEventListener('click', () => openM(+c.dataset.dwI)));
modal.querySelectorAll('[data-dw-close]').forEach(b => b.addEventListener('click', closeM));
q('.dw-m-next').addEventListener('click', () => fill(idx + 1));
document.addEventListener('keydown', e => {
if(!open) return;
if(e.key === 'Escape'){ e.preventDefault(); closeM(); return; }
if(e.key === 'Tab'){
const f = focusables(); if(!f.length) return;
const first = f[0], last = f[f.length - 1];
if(e.shiftKey && (document.activeElement === first || !drawer.contains(document.activeElement))){ e.preventDefault(); last.focus(); }
else if(!e.shiftKey && (document.activeElement === last || !drawer.contains(document.activeElement))){ e.preventDefault(); first.focus(); }
}
});
DD.refresh();
});
</script>
<section class="ds" id="studio" aria-labelledby="ds-title">
<div class="ds-bento">
<!-- 1. about -->
<div class="cell pad ds-about">
<p class="px ds-eyebrow"><span>(03)</span> The studio <span class="ds-mute">/ Noordereiland</span></p>
<h2 class="statement ds-state" id="ds-title">We keep a very old machine alive and print things far louder than they have any right to be.</h2>
<div class="ds-body">
<p>Double Drum Press is Mira Okafor on the drums and Sem de Wit at the drawing table, sharing one room on Maaskade since 2019. We print short runs of 50 to 2,000 for bands, festivals, artists and anyone who would rather hold a poster than scroll past one.</p>
<p>Soy-based ink, recycled stock, and the occasional happy accident we insist was on purpose.</p>
<p class="px ds-mute">Est. 2019 · 2 people · 2 drums · 1 kettle</p>
</div>
</div>
<!-- 2. riso portrait -->
<figure class="cell ds-photo">
<div class="ds-par">
<div class="ds-riso" data-ds-riso>
<div class="ds-l ds-l--sun"><img src="assets/printer.jpg" alt="Mira standing beside the two-drum risograph in the studio" loading="lazy"></div>
<div class="ds-l ds-l--tom" aria-hidden="true"><img src="assets/printer.jpg" alt="" loading="lazy"></div>
</div>
</div>
<figcaption class="px ds-tag">Mira + the Beast</figcaption>
</figure>
<!-- 3. running machines -->
<div class="cell pad ds-machines">
<div class="ds-mhead">
<h3 class="px ds-mtitle">Running machines</h3>
<span class="px ds-live"><span class="ds-dot ds-dot--blink"></span> Live</span>
</div>
<ul class="ds-rows">
<li class="ds-row ds-row--beast">
<button type="button" class="ds-beast" aria-expanded="false" aria-controls="ds-readout">
<span class="ds-name">The Beast <span class="ds-sub">2-drum · sun + tomato</span></span>
<span class="ds-stat">Printing <span class="ds-dot ds-dot--print"></span></span>
</button>
</li>
<li class="ds-row"><span class="ds-name">Kevin <span class="ds-sub">1-drum backup · blue</span></span><span class="ds-stat">Idle <span class="ds-dot ds-dot--idle"></span></span></li>
<li class="ds-row"><span class="ds-name">Guillotine</span><span class="ds-stat">Sharp <span class="ds-dot ds-dot--sun"></span></span></li>
<li class="ds-row"><span class="ds-name">Kettle</span><span class="ds-stat">On <span class="ds-dot ds-dot--sun ds-dot--blink"></span></span></li>
<li class="ds-row"><span class="ds-name">Workshop bench</span><span class="ds-stat">Open Thu <span class="ds-dot ds-dot--sun ds-dot--blink"></span></span></li>
<li class="ds-row"><span class="ds-name">Webshop</span><span class="ds-stat">Online <span class="ds-dot ds-dot--sun ds-dot--blink"></span></span></li>
</ul>
<p class="px ds-hint ds-mute">↑ Poke the Beast for a readout</p>
<div class="ds-readout" id="ds-readout" role="region" aria-label="The Beast live readout" hidden>
<div class="ds-rtop">
<span class="px">Beast / run 3 of 5</span>
<button type="button" class="px ds-close" aria-label="Close readout">Close ×</button>
</div>
<svg class="ds-drums" viewBox="0 0 220 96" aria-hidden="true">
<g class="ds-drum ds-drum--a">
<circle cx="56" cy="48" r="38" class="ds-drum-sun"/>
<line x1="56" y1="12" x2="56" y2="84"/><line x1="20" y1="48" x2="92" y2="48"/>
<circle cx="56" cy="48" r="7" class="ds-hub"/>
</g>
<g class="ds-drum ds-drum--b">
<circle cx="164" cy="48" r="38" class="ds-drum-tom"/>
<line x1="164" y1="12" x2="164" y2="84"/><line x1="128" y1="48" x2="200" y2="48"/>
<circle cx="164" cy="48" r="7" class="ds-hub"/>
</g>
<line x1="0" y1="93" x2="220" y2="93" class="ds-paper"/>
</svg>
<div class="ds-count">
<span class="px ds-mute">Sheets today</span>
<span class="ds-num" aria-live="off">1,248</span>
</div>
<div class="ds-ink">
<div class="ds-bar"><span class="px">Sun</span><span class="ds-track"><span class="ds-fill ds-fill--sun"></span></span><span class="px ds-pct" data-k="sun">78%</span></div>
<div class="ds-bar"><span class="px">Tomato</span><span class="ds-track"><span class="ds-fill ds-fill--tom"></span></span><span class="px ds-pct" data-k="tom">64%</span></div>
</div>
<p class="px ds-spec"><span>Stock: recycled 90g</span><span>Speed: 130 ppm</span><span>Jams today: 1 (Kevin's fault)</span></p>
</div>
</div>
</div>
<!-- 4. workshops band -->
<div class="cell pad ds-band">
<div class="ds-band-l">
<p class="px">Workshops — monthly</p>
<p class="big ds-big">First<br>Thursdays</p>
</div>
<div class="ds-band-r">
<p class="ds-lede">Three hours on the Beast with Mira. You draw two layers, we burn two masters, you pull your own print. Nobody leaves clean.</p>
<dl class="px ds-facts">
<div><dt>Price</dt><dd>€45</dd></div>
<div><dt>Seats</dt><dd>8</dd></div>
<div><dt>Time</dt><dd>19:00–22:00</dd></div>
<div><dt>Bring</dt><dd>Nothing</dd></div>
<div><dt>Leave with</dt><dd>A two-colour poster</dd></div>
</dl>
<div class="ds-seats-row">
<div class="ds-seats" role="img" aria-label="6 of 8 seats booked, 2 left for October">
<span class="ds-seat"></span><span class="ds-seat"></span><span class="ds-seat"></span><span class="ds-seat"></span>
<span class="ds-seat"></span><span class="ds-seat"></span><span class="ds-seat ds-seat--left"></span><span class="ds-seat ds-seat--left"></span>
</div>
<span class="px">6 booked · <span class="ds-left">2 left</span></span>
</div>
<a class="btn" href="#contact">Book a seat <span aria-hidden="true">→</span></a>
</div>
</div>
<!-- 5. studio strip -->
<figure class="cell ds-strip">
<div class="ds-wipe">
<div class="ds-riso ds-riso--strip" data-ds-riso>
<div class="ds-l ds-l--sun"><img src="assets/studio.jpg" alt="Printer feeding paper through the press rollers at Double Drum Press" loading="lazy"></div>
<div class="ds-l ds-l--tom" aria-hidden="true"><img src="assets/studio.jpg" alt="" loading="lazy"></div>
</div>
</div>
<figcaption class="px ds-tag ds-tag--r">Maaskade 112 · press room · 14:20</figcaption>
</figure>
</section>
<style>
.ds{scroll-margin-top:80px}
.ds-bento{display:grid;grid-template-columns:1.15fr 1fr 1fr}
.ds-bento > .cell:last-child,.ds-band,.ds-strip{border-right:0}
.ds-mute{color:var(--mute)}
.ds p{margin:0}
/* about */
.ds-about{background:var(--grey);display:flex;flex-direction:column;justify-content:space-between;gap:48px;min-height:clamp(520px,44vw,660px)}
.ds-eyebrow{display:flex;gap:.8em}
.ds-state{margin-top:24px;max-width:15ch;font-size:clamp(28px,3vw,46px)}
.ds-body{display:grid;gap:12px;max-width:44ch;color:var(--ink)}
.ds-body .px{margin-top:6px}
/* riso print (shared by portrait + strip) */
.ds-riso{position:relative;width:100%;height:100%;isolation:isolate;overflow:hidden;background:var(--bg)}
.ds-l{position:absolute;inset:-18px;background:var(--bg);mix-blend-mode:multiply;isolation:isolate}
.ds-l img{width:100%;height:100%;object-fit:cover}
.ds-l::after{content:"";position:absolute;inset:0;mix-blend-mode:screen}
.ds-l--sun img{filter:none}
.ds-l--sun::after{display:none}
.ds-l--tom{display:none}
.ds-l--tom::after{background:var(--tomato)}
.ds-l--sun{transform:translate(-7px,5px)}
.ds-l--tom{transform:translate(8px,-6px)}
.ds-photo{margin:0;position:relative;overflow:hidden;padding:0;min-height:420px}
.ds-par{position:absolute;left:0;right:0;top:-9%;height:118%;will-change:transform}
.ds-tag{position:absolute;left:14px;bottom:14px;margin:0;background:var(--bg);border:var(--hair);padding:6px 9px;z-index:2}
.ds-tag--r{left:auto;right:14px}
/* machines */
.ds-machines{position:relative;display:flex;flex-direction:column}
.ds-mhead{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px;border-bottom:var(--hair)}
.ds-mtitle{margin:0;font-size:13px}
.ds-live{display:inline-flex;align-items:center;gap:6px}
.ds-rows{list-style:none;margin:0;padding:0}
.ds-row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:13px 0;border-bottom:1px dashed rgba(31,28,43,.35);font-family:var(--pixel);font-size:12px;text-transform:uppercase;line-height:1.3}
.ds-row--beast{padding:0}
.ds-beast{display:flex;justify-content:space-between;align-items:center;gap:12px;width:calc(100% + 16px);margin-inline:-8px;padding:13px 8px;text-align:left;text-transform:uppercase;font-family:var(--pixel);font-size:12px;line-height:1.3;transition:background .2s var(--ease),padding .2s var(--ease)}
.ds-beast:hover,.ds-beast[aria-expanded="true"]{background:var(--tomato-soft)}
.ds-name{display:flex;flex-direction:column;gap:3px}
.ds-sub{font-size:9px;color:var(--mute);letter-spacing:.04em}
.ds-stat{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}
.ds-hint{margin-top:auto;padding-top:18px}
.ds-dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--tomato);flex:none}
.ds-dot--sun{background:var(--sun);box-shadow:inset 0 0 0 1px var(--ink)}
.ds-dot--idle{background:transparent;box-shadow:inset 0 0 0 1.5px var(--ink)}
.ds-live .ds-dot{background:var(--tomato)}
/* readout overlay (revealed with clip-path, no layout shift) */
.ds-readout{position:absolute;left:clamp(14px,1.6vw,22px);right:clamp(14px,1.6vw,22px);bottom:clamp(14px,1.6vw,22px);top:120px;
background:var(--bg);border:var(--hair);padding:14px;display:flex;flex-direction:column;gap:12px;z-index:3;clip-path:inset(0 0 100% 0)}
.ds-readout[hidden]{display:none}
.ds-rtop{display:flex;justify-content:space-between;align-items:center}
.ds-close{padding:4px 6px;border:var(--hair);border-radius:999px;font-size:10px}
.ds-close:hover{background:var(--sun)}
.ds-drums{width:100%;max-width:300px;height:auto;display:block;overflow:visible}
.ds-drums line{stroke:var(--ink);stroke-width:1.5}
.ds-drums circle{stroke:var(--ink);stroke-width:1.5}
.ds-drum-sun{fill:var(--sun)}
.ds-drum-tom{fill:var(--tomato)}
.ds-hub{fill:var(--bg)}
.ds-drums .ds-paper{stroke-dasharray:10 6;stroke-width:2}
.ds-count{display:flex;justify-content:space-between;align-items:baseline;border-top:var(--hair);padding-top:10px}
.ds-num{font-family:var(--display);font-weight:900;font-size:40px;line-height:.9;font-variant-numeric:tabular-nums}
.ds-ink{display:grid;gap:8px}
.ds-bar{display:grid;grid-template-columns:54px 1fr 38px;align-items:center;gap:8px}
.ds-track{height:10px;border:var(--hair);position:relative;overflow:hidden}
.ds-fill{position:absolute;inset:0;transform-origin:0 50%;transform:scaleX(.7)}
.ds-fill--sun{background:var(--sun)}
.ds-fill--tom{background:var(--tomato)}
.ds-pct{text-align:right}
.ds-spec{margin-top:auto;display:flex;flex-wrap:wrap;gap:4px 14px;border-top:1px dashed rgba(31,28,43,.35);padding-top:10px;color:var(--mute)}
/* workshops band */
.ds-band{background:var(--sun);display:grid;grid-template-columns:1.15fr 2fr;gap:clamp(24px,4vw,64px);align-items:end;padding-block:clamp(28px,4vw,56px)}
.ds-big{font-size:clamp(64px,9.5vw,168px);margin-top:14px}
.ds-band-r{display:grid;gap:20px;justify-items:start}
.ds-lede{font-size:clamp(17px,1.4vw,21px);line-height:1.35;max-width:46ch;font-weight:500}
.ds-facts{display:grid;grid-template-columns:repeat(5,auto);margin:0;border-top:var(--hair);border-left:var(--hair);width:100%}
.ds-facts > div{border-right:var(--hair);border-bottom:var(--hair);padding:10px 12px}
.ds-facts dt{color:rgba(31,28,43,.7);font-size:9px;margin-bottom:4px}
.ds-facts dd{margin:0;font-size:13px}
.ds-seats-row{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.ds-seats{display:flex;gap:6px}
.ds-seat{width:22px;height:22px;border:1.5px solid var(--ink);position:relative;
background:linear-gradient(45deg,transparent calc(50% - 1px),var(--ink) calc(50% - 1px),var(--ink) calc(50% + 1px),transparent calc(50% + 1px))}
.ds-seat--left{background:var(--tomato);border-color:var(--tomato-deep)}
.ds-left{color:var(--tomato-deep)}
/* strip */
.ds-strip{margin:0;padding:0;position:relative;height:clamp(180px,24vw,360px);overflow:hidden}
.ds-wipe{position:absolute;inset:0}
.ds-riso--strip img{object-position:50% 45%}
@media (max-width:1100px){
.ds-bento{grid-template-columns:1fr 1fr}
.ds-about{grid-column:1 / -1;min-height:0;border-right:0}
.ds-photo{min-height:520px}
.ds-facts{grid-template-columns:repeat(3,auto)}
.ds-band{grid-template-columns:1fr}
}
@media (max-width:720px){
.ds-bento{grid-template-columns:1fr}
.ds-bento > .cell{border-right:0}
.ds-bento .pad,.ds-band.pad{padding-inline:16px}
.ds-readout{left:16px;right:16px}
.ds-photo{min-height:0;aspect-ratio:4/5}
.ds-machines{min-height:520px}
.ds-facts{grid-template-columns:1fr 1fr}
.ds-facts > div:last-child{grid-column:1 / -1}
.ds-strip{height:200px}
.ds-tag{left:16px;bottom:16px}
.ds-tag--r{left:auto;right:16px}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.ds-l--sun,.ds-l--tom{transform:none}
.ds-beast{transition:none}
}
</style>
<script>
DD.later(() => {
const root = document.getElementById('studio');
if (!root) return;
const R = DD.reduce, q = s => root.querySelector(s), qa = s => [...root.querySelectorAll(s)];
const fine = matchMedia('(hover:hover) and (pointer:fine)').matches;
const loops = []; // everything that runs forever; paused offscreen
let inView = false;
const readout = q('.ds-readout'), beast = q('.ds-beast'), closeBtn = q('.ds-close');
const num = q('.ds-num'), fills = { sun: q('.ds-fill--sun'), tom: q('.ds-fill--tom') };
const pcts = { sun: q('.ds-pct[data-k="sun"]'), tom: q('.ds-pct[data-k="tom"]') };
const ink = { sun: 78, tom: 64 };
let sheets = 1248;
const fmt = n => n.toLocaleString('en-US');
const setInk = (k, anim) => {
pcts[k].textContent = Math.round(ink[k]) + '%';
if (anim && !R) gsap.to(fills[k], { scaleX: ink[k] / 100, duration: .35, ease: 'power2.out', overwrite: true });
else gsap.set(fills[k], { scaleX: ink[k] / 100 });
};
setInk('sun'); setInk('tom');
/* ---- readout open / close ---- */
let open = false, pinned = false;
const setTop = () => { const row = q('.ds-row--beast'); readout.style.top = (row.offsetTop + row.offsetHeight + 8) + 'px'; };
function show(v){
if (v === open) return; open = v;
beast.setAttribute('aria-expanded', v);
if (v){
setTop(); readout.hidden = false;
if (R) gsap.set(readout, { clipPath: 'inset(0 0 0% 0)' });
else {
gsap.to(readout, { clipPath: 'inset(0 0 0% 0)', duration: .55, ease: 'expo.out', overwrite: true });
gsap.fromTo(fills.sun, { scaleX: 0 }, { scaleX: ink.sun / 100, duration: .9, ease: 'expo.out', delay: .15 });
gsap.fromTo(fills.tom, { scaleX: 0 }, { scaleX: ink.tom / 100, duration: .9, ease: 'expo.out', delay: .25 });
}
} else {
if (R){ gsap.set(readout, { clipPath: 'inset(0 0 100% 0)' }); readout.hidden = true; }
else gsap.to(readout, { clipPath: 'inset(0 0 100% 0)', duration: .35, ease: 'power2.in', overwrite: true, onComplete: () => { if (!open) readout.hidden = true; } });
}
}
beast.addEventListener('click', () => { if (open && pinned){ pinned = false; show(false); } else { pinned = true; show(true); } });
closeBtn.addEventListener('click', () => { pinned = false; show(false); beast.focus(); });
if (fine){
const box = q('.ds-machines');
beast.addEventListener('mouseenter', () => show(true));
box.addEventListener('mouseleave', () => { if (!pinned) show(false); });
}
root.addEventListener('keydown', e => { if (e.key === 'Escape' && open){ pinned = false; show(false); beast.focus(); } });
addEventListener('resize', () => { if (open) setTop(); });
/* ---- live counter + ink drain (0.4s per sheet) ---- */
const tick = () => {
sheets++; num.textContent = fmt(sheets);
ink.sun -= .35; ink.tom -= .5;
['sun', 'tom'].forEach(k => {
if (ink[k] < 16){ ink[k] = 94; setInk(k, true); if (!R) gsap.fromTo(fills[k], { opacity: .3 }, { opacity: 1, duration: .4, ease: 'steps(3)' }); }
else setInk(k, open);
});
if (open && !R) gsap.fromTo(num, { yPercent: -18 }, { yPercent: 0, duration: .3, ease: 'power2.out' });
};
num.textContent = fmt(sheets);
const counter = gsap.timeline({ repeat: -1, paused: true }).call(tick, null, .4);
loops.push(counter);
if (!R){
/* drums turn together, like a real two-drum feed */
loops.push(gsap.to('#studio .ds-drum--a', { rotation: 360, svgOrigin: '56 48', duration: 1.6, ease: 'none', repeat: -1, paused: true }));
loops.push(gsap.to('#studio .ds-drum--b', { rotation: 360, svgOrigin: '164 48', duration: 1.6, ease: 'none', repeat: -1, paused: true }));
loops.push(gsap.to('#studio .ds-paper', { strokeDashoffset: -32, duration: .4, ease: 'none', repeat: -1, paused: true }));
/* status dots */
loops.push(gsap.fromTo(qa('.ds-dot--print'), { scale: 1, opacity: 1 }, { scale: 1.6, opacity: .45, duration: .7, ease: 'sine.inOut', yoyo: true, repeat: -1, paused: true }));
qa('.ds-dot--blink').forEach((d, i) => loops.push(gsap.to(d, { opacity: .12, duration: .5 + i * .13, ease: 'steps(1)', yoyo: true, repeat: -1, paused: true })));
}
ScrollTrigger.create({ trigger: root, start: 'top bottom', end: 'bottom top',
onToggle: s => { inView = s.isActive; loops.forEach(l => inView ? l.play() : l.pause()); } });
if (R){ DD.refresh(); return; }
/* ---- headline + body ---- */
DD.reveal(q('.ds-state'), { start: 'top 85%' });
gsap.from(qa('.ds-body > p, .ds-eyebrow'), { y: 18, opacity: 0, duration: .8, ease: 'power3.out', stagger: .08,
scrollTrigger: { trigger: q('.ds-about'), start: 'top 75%', once: true } });
/* ---- riso prints: register on scroll-in, slip on hover ---- */
qa('[data-ds-riso]').forEach(p => {
const sun = p.querySelector('.ds-l--sun'), tom = p.querySelector('.ds-l--tom');
gsap.set(sun, { x: -14, y: 10 }); gsap.set(tom, { x: 16, y: -12 });
gsap.to([sun, tom], { x: 0, y: 0, duration: 1.3, ease: 'expo.out', stagger: .1,
scrollTrigger: { trigger: p, start: 'top 80%', once: true } });
if (fine){
p.parentElement.closest('figure').addEventListener('mouseenter', () => {
gsap.timeline({ overwrite: true })
.to(sun, { x: -6, y: 4, duration: .18, ease: 'power2.out' }, 0)
.to(tom, { x: 7, y: -5, duration: .18, ease: 'power2.out' }, 0)
.to([sun, tom], { x: 0, y: 0, duration: .9, ease: 'elastic.out(1,.4)' }, .22);
});
}
});
/* portrait parallax, scrubbed */
gsap.fromTo(q('.ds-par'), { yPercent: -6 }, { yPercent: 6, ease: 'none',
scrollTrigger: { trigger: q('.ds-photo'), start: 'top bottom', end: 'bottom top', scrub: true } });
/* machine rows stagger in */
gsap.from(qa('.ds-row, .ds-mhead, .ds-hint'), { x: -16, opacity: 0, duration: .6, ease: 'power3.out', stagger: .07,
scrollTrigger: { trigger: q('.ds-machines'), start: 'top 75%', once: true } });
/* workshops */
DD.reveal(q('.ds-big'), { mode: 'chars', start: 'top 88%' });
gsap.from(qa('.ds-lede, .ds-facts > div, .ds-band-r > .btn'), { y: 16, opacity: 0, duration: .7, ease: 'power3.out', stagger: .05,
scrollTrigger: { trigger: q('.ds-band-r'), start: 'top 82%', once: true } });
const seats = qa('.ds-seat');
gsap.set(seats, { scale: 0, opacity: 0 });
gsap.to(seats, { scale: 1, opacity: 1, duration: .35, ease: 'back.out(2.4)', stagger: .09,
scrollTrigger: { trigger: q('.ds-seats'), start: 'top 90%', once: true },
onComplete: () => gsap.to(qa('.ds-seat--left'), { opacity: .35, duration: .45, ease: 'steps(1)', yoyo: true, repeat: 5 }) });
/* studio strip: horizontal scrub wipe + registration along the way */
const strip = q('.ds-strip');
const tl = gsap.timeline({ scrollTrigger: { trigger: strip, start: 'top 95%', end: 'bottom 55%', scrub: .6 } });
tl.fromTo(q('.ds-wipe'), { clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0% 0 0)', ease: 'none' }, 0);
tl.fromTo(strip.querySelector('.ds-riso'), { scale: 1.12 }, { scale: 1, ease: 'none' }, 0);
DD.refresh();
});
</script>
<section id="shop" class="dp" aria-labelledby="dp-title">
<h2 id="dp-title" class="dp-sr">Shop the press</h2>
<!-- giant ticker -->
<div class="dp-tick" aria-hidden="true">
<div class="dp-track"><span class="dp-unit"><span class="big dp-w">Shop the press</span><span class="big dp-star">✦</span><span class="big dp-w dp-w--o">Shop the press</span><span class="big dp-star">✦</span></span></div>
</div>
<div class="dp-body">
<!-- left column -->
<aside class="dp-side" aria-label="Shop information">
<div class="dp-side-top pad">
<p class="px dp-kicker"><span class="dot dp-blink" aria-hidden="true"></span> Webshop — online · 06 items</p>
<p class="statement dp-statement">Limited runs. Printed here. Posted with a handwritten note.</p>
<p class="dp-small">Everything is printed on the Beast in our studio on the Noordereiland, then wrapped by whoever has the cleanest hands that day. Parcels leave Rotterdam every Tuesday and Friday.</p>
<dl class="px dp-ship">
<div><dt>NL</dt><dd>€4.50 · 1–2 days · free over €60</dd></div>
<div><dt>EU</dt><dd>€9.50 · 3–6 days</dd></div>
<div><dt>Rest</dt><dd>Email us, we'll figure it out</dd></div>
</dl>
</div>
<div class="dp-dl pad">
<p class="big dp-dl-big">Free<br>stuff</p>
<p class="dp-dl-note">Riso textures for your phone and desk, plus every ink we own as a printable swatch card.</p>
<p class="px dp-dl-head">Downloads <span class="dp-dl-free">free</span></p>
<ul class="dp-dl-list">
<li class="px"><span class="dp-dl-name">Wallpaper pack 01</span><span class="dp-dl-links"><a class="link" href="#">details</a> / <a class="link" href="#">download</a></span></li>
<li class="px"><span class="dp-dl-name">Wallpaper pack 02</span><span class="dp-dl-links"><a class="link" href="#">details</a> / <a class="link" href="#">download</a></span></li>
<li class="px"><span class="dp-dl-name">Colour swatch PDF</span><span class="dp-dl-links"><a class="link" href="#">details</a> / <a class="link" href="#">download</a></span></li>
</ul>
</div>
</aside>
<!-- product grid -->
<ul class="dp-grid" aria-label="Products">
<li class="dp-item" data-name="Drum Solo no.7" data-price="12">
<div class="dp-fig">
<img class="dp-photo" src="assets/w6.jpg" alt="A stack of freshly printed zines and magazines" loading="lazy">
<div class="dp-riso" aria-hidden="true">
<div class="dp-layer dp-layer--sun"><img src="assets/w6.jpg" alt="" loading="lazy"></div>
<div class="dp-layer dp-layer--tom"><img src="assets/w6.jpg" alt="" loading="lazy"></div>
</div>
<span class="px dp-tag">New issue</span>
</div>
<div class="dp-cap pad">
<div class="dp-cap-txt">
<h3 class="px dp-name">Drum Solo no.7 — €12</h3>
<p class="dp-meta">Zine · 40 pages · A5 · sunflower + tomato</p>
</div>
<button class="btn dp-add" type="button" aria-label="Add Drum Solo no.7 to cart, €12"><span class="dp-add-l">Add +</span></button>
</div>
</li>
<li class="dp-item" data-name="Misregistered poster A2" data-price="35">
<div class="dp-fig">
<img class="dp-photo" src="assets/w2.jpg" alt="A wall covered in brightly coloured posters" loading="lazy">
<div class="dp-riso" aria-hidden="true">
<div class="dp-layer dp-layer--sun"><img src="assets/w2.jpg" alt="" loading="lazy"></div>
<div class="dp-layer dp-layer--tom"><img src="assets/w2.jpg" alt="" loading="lazy"></div>
</div>
<span class="px dp-tag">Last 9</span>
</div>
<div class="dp-cap pad">
<div class="dp-cap-txt">
<h3 class="px dp-name">Misregistered — €35</h3>
<p class="dp-meta">Poster · A2 · off on purpose, mostly</p>
</div>
<button class="btn dp-add" type="button" aria-label="Add Misregistered poster A2 to cart, €35"><span class="dp-add-l">Add +</span></button>
</div>
</li>
<li class="dp-item" data-name="Tote “Print Loud”" data-price="22">
<div class="dp-fig">
<img class="dp-photo" src="assets/s1.jpg" alt="A cotton tote bag with a bold printed design" loading="lazy">
<div class="dp-riso" aria-hidden="true">
<div class="dp-layer dp-layer--sun"><img src="assets/s1.jpg" alt="" loading="lazy"></div>
<div class="dp-layer dp-layer--tom"><img src="assets/s1.jpg" alt="" loading="lazy"></div>
</div>
</div>
<div class="dp-cap pad">
<div class="dp-cap-txt">
<h3 class="px dp-name">Tote “Print Loud” — €22</h3>
<p class="dp-meta">Heavy cotton · carries 30 zines, tested</p>
</div>
<button class="btn dp-add" type="button" aria-label="Add Tote Print Loud to cart, €22"><span class="dp-add-l">Add +</span></button>
</div>
</li>
<li class="dp-item" data-name="Tee “Ink Stained”" data-price="30">
<div class="dp-fig">
<img class="dp-photo" src="assets/s2.jpg" alt="A white t-shirt with a large printed slogan" loading="lazy">
<div class="dp-riso" aria-hidden="true">
<div class="dp-layer dp-layer--sun"><img src="assets/s2.jpg" alt="" loading="lazy"></div>
<div class="dp-layer dp-layer--tom"><img src="assets/s2.jpg" alt="" loading="lazy"></div>
</div>
</div>
<div class="dp-cap pad">
<div class="dp-cap-txt">
<h3 class="px dp-name">Tee “Ink Stained” — €30</h3>
<p class="dp-meta">Organic cotton · S–XXL · stains not included</p>
</div>
<button class="btn dp-add" type="button" aria-label="Add Tee Ink Stained to cart, €30"><span class="dp-add-l">Add +</span></button>
</div>
</li>
<li class="dp-item" data-name="Sticker sheet “Kevin Lives”" data-price="6">
<div class="dp-fig">
<img class="dp-photo" src="assets/s3.jpg" alt="A sheet of colourful printed stickers" loading="lazy">
<div class="dp-riso" aria-hidden="true">
<div class="dp-layer dp-layer--sun"><img src="assets/s3.jpg" alt="" loading="lazy"></div>
<div class="dp-layer dp-layer--tom"><img src="assets/s3.jpg" alt="" loading="lazy"></div>
</div>
<span class="px dp-tag">Kevin approved</span>
</div>
<div class="dp-cap pad">
<div class="dp-cap-txt">
<h3 class="px dp-name">Kevin Lives — €6</h3>
<p class="dp-meta">Sticker sheet · A6 · 12 tiny Kevins</p>
</div>
<button class="btn dp-add" type="button" aria-label="Add Sticker sheet Kevin Lives to cart, €6"><span class="dp-add-l">Add +</span></button>
</div>
</li>
<li class="dp-item dp-item--out" data-name="Framed poster set" data-price="95">
<div class="dp-fig">
<img class="dp-photo" src="assets/s4.jpg" alt="A set of framed posters hanging on a wall" loading="lazy">
<div class="dp-riso" aria-hidden="true">
<div class="dp-layer dp-layer--sun"><img src="assets/s4.jpg" alt="" loading="lazy"></div>
<div class="dp-layer dp-layer--tom"><img src="assets/s4.jpg" alt="" loading="lazy"></div>
</div>
<span class="dp-stamp" aria-hidden="true"><span class="big">Sold out</span></span>
</div>
<div class="dp-cap pad">
<div class="dp-cap-txt">
<h3 class="px dp-name">Framed set — €95</h3>
<p class="dp-meta">3 × A3 in oak · back in the new year</p>
</div>
<button class="btn dp-add" type="button" disabled aria-label="Framed poster set is sold out"><span class="dp-add-l">Sold out</span></button>
</div>
</li>
</ul>
</div>
<div class="dp-toast" role="status" aria-live="polite">
<span class="px dp-toast-k"><span class="dot" aria-hidden="true"></span> Cart</span>
<span class="dp-toast-t"></span>
</div>
</section>
<style>
.dp{scroll-margin-top:84px;position:relative}
.dp-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
/* ticker — margin-top:-1px lets the top hairline sit on (not next to) the previous section's last line */
.dp-tick{overflow:hidden;white-space:nowrap;border-top:var(--hair);border-bottom:var(--hair);margin-top:-1px;padding:1.2vw 0 .5vw}
.dp-track{display:flex;width:max-content}
.dp-unit{display:inline-flex;align-items:center;flex:none}
.dp-unit .big{font-size:12vw;line-height:.9;padding-right:.22em;flex:none}
.dp-w--o{color:transparent;-webkit-text-stroke:2px var(--ink)}
.dp-star{color:var(--tomato);font-size:7vw !important;font-family:var(--body) !important;transform:translateY(-.08em)}
/* body grid */
.dp-body{display:grid;grid-template-columns:minmax(280px,26%) 1fr}
.dp-side{display:flex;flex-direction:column;border-right:var(--hair);border-bottom:var(--hair)}
.dp-side-top{display:flex;flex-direction:column;gap:18px;padding-bottom:28px}
.dp-kicker{margin:0;display:flex;align-items:center;gap:8px}
.dp-statement{font-size:clamp(24px,2.3vw,36px)}
.dp-small{margin:0;color:var(--mute);font-size:14px;max-width:34ch}
.dp-ship{margin:0;display:grid;border-top:var(--hair)}
.dp-ship div{display:grid;grid-template-columns:44px 1fr;gap:8px;padding:8px 0;border-bottom:1px dashed rgba(31,28,43,.35)}
.dp-ship dt{color:var(--tomato)}
.dp-ship dd{margin:0}
.dp-dl{background:var(--sun);border-top:var(--hair);flex:1;display:flex;flex-direction:column;justify-content:flex-end;gap:10px;min-height:190px}
.dp-dl-big{font-size:clamp(64px,7vw,120px);margin-bottom:auto}
.dp-dl-note{margin:0 0 8px;font-size:14px;max-width:30ch}
.dp-dl-head{margin:0;font-size:13px;display:flex;align-items:center;gap:8px}
.dp-dl-free{border:var(--hair);border-radius:999px;padding:1px 8px;font-size:10px;background:var(--bg)}
.dp-dl-list{list-style:none;margin:0;padding:0;border-top:var(--hair)}
.dp-dl-list li{display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap;padding:9px 0;border-bottom:var(--hair)}
.dp-dl-links{white-space:nowrap}
.dp-dl-links a{text-decoration-thickness:1px}
.dp-dl-links a:hover{color:var(--tomato-deep)}
.dp-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,1fr)}
.dp-item{display:flex;flex-direction:column;border-bottom:var(--hair);border-right:var(--hair);min-width:0}
.dp-item:nth-child(3n){border-right:0}
.dp-fig{position:relative;aspect-ratio:4/5;overflow:hidden;background:var(--grey);border-bottom:var(--hair);isolation:isolate}
.dp-fig img{width:100%;height:100%;object-fit:cover}
.dp-photo{position:relative;z-index:0}
.dp-riso{display:none;position:absolute;inset:0;z-index:1;opacity:0;background:var(--bg);isolation:isolate;transition:opacity .45s var(--ease)}
.dp-layer{position:absolute;inset:-10px;background:var(--bg);isolation:isolate}
.dp-layer img{filter:grayscale(1) contrast(1.25) brightness(1.08)}
.dp-layer::after{content:"";position:absolute;inset:0;mix-blend-mode:screen}
.dp-layer--sun{transform:translate(-6px,5px)}
.dp-layer--sun::after{background:var(--sun)}
.dp-layer--tom{mix-blend-mode:multiply;transform:translate(6px,-4px)}
.dp-layer--tom img{filter:grayscale(1) contrast(2.4) brightness(1.4)}
.dp-layer--tom::after{background:var(--tomato)}
.dp-item:hover .dp-riso,.dp-item:focus-within .dp-riso{opacity:1}
.dp-item--out .dp-photo{filter:grayscale(.35)}
.dp-tag{position:absolute;z-index:2;top:12px;left:12px;background:var(--bg);border:var(--hair);border-radius:999px;padding:4px 9px;font-size:10px}
.dp-stamp{position:absolute;z-index:3;left:50%;top:50%;transform:translate(-50%,-50%) rotate(-16deg);background:var(--tomato);color:#fff;
padding:.18em .5em .1em;border:2px solid #fff;outline:2px solid var(--tomato);pointer-events:none;white-space:nowrap}
.dp-stamp .big{font-size:clamp(40px,4.4vw,68px);line-height:.9}
.dp-cap{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:auto}
.dp-cap-txt{min-width:0}
.dp-name{margin:0;font-size:12px;font-weight:400}
.dp-meta{margin:4px 0 0;font-size:13px;color:var(--mute);line-height:1.35}
.dp-add{flex:none;overflow:hidden;min-width:92px;justify-content:center}
.dp-add-l{display:inline-block}
.dp-add.is-done{background:var(--tomato);color:#fff}
.dp-add:disabled{background:var(--grey);color:var(--mute);cursor:not-allowed;text-decoration:line-through}
.dp-add:disabled:hover{background:var(--grey);color:var(--mute)}
.dp-blink{animation:dp-blink 1.2s steps(1) infinite}
@keyframes dp-blink{50%{opacity:0}}
/* toast + flying sticker */
.dp-toast{position:fixed;right:20px;bottom:20px;z-index:60;background:var(--bg);border:var(--hair);display:flex;align-items:center;gap:14px;
padding:14px 18px;max-width:min(420px,calc(100vw - 32px));transform:translateY(calc(100% + 40px));visibility:hidden;font-size:14px}
.dp-toast-k{display:flex;align-items:center;gap:6px;padding-right:14px;border-right:var(--hair);align-self:stretch}
.dp-fly{position:fixed;left:0;top:0;z-index:70;width:38px;height:38px;margin:-19px 0 0 -19px;border-radius:50%;background:var(--sun);border:var(--hair);
display:grid;place-items:center;font-family:var(--pixel);font-size:12px;color:var(--ink);pointer-events:none}
@media (max-width:1099px){
.dp-body{grid-template-columns:1fr}
.dp-side{border-right:0;display:grid;grid-template-columns:1.3fr 1fr}
.dp-dl{border-top:0;border-left:var(--hair)}
.dp-grid{grid-template-columns:repeat(2,1fr)}
.dp-item:nth-child(3n){border-right:var(--hair)}
.dp-item:nth-child(2n){border-right:0}
}
@media (max-width:699px){
.dp-side{display:flex}
.dp-dl{border-left:0;border-top:var(--hair);min-height:0}
.dp-side-top,.dp-dl,.dp-cap{padding-inline:16px}
.dp-grid{grid-template-columns:1fr}
.dp-item:nth-child(n){border-right:0}
.dp-w--o{-webkit-text-stroke:1px var(--ink)}
.dp-toast{left:16px;right:16px;bottom:16px}
.dp-stamp .big{font-size:52px}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.dp-blink{animation:none}
.dp-riso{transition:none}
.dp-layer--sun,.dp-layer--tom{transform:none}
}
</style>
<script>
DD.later(() => {
const root = document.getElementById('shop');
if(!root) return;
const R = DD.reduce;
/* 1 — giant ticker */
DD.ticker(root.querySelector('.dp-track'), { speed:70, react:true });
/* 2 — riso hover: crossfade is CSS, GSAP wobbles the two drums into register */
const hover = matchMedia('(hover:hover)').matches;
root.querySelectorAll('.dp-item').forEach(item => {
const sun = item.querySelector('.dp-layer--sun'), tom = item.querySelector('.dp-layer--tom');
if(R || !sun) return;
const wobble = () => {
gsap.killTweensOf([sun, tom]);
gsap.fromTo(sun, { x:-9, y:6, rotation:-.8 }, { x:0, y:0, rotation:0, duration:1.1, ease:'elastic.out(1,.35)' });
gsap.fromTo(tom, { x:9, y:-5, rotation:.8 }, { x:0, y:0, rotation:0, duration:1.1, ease:'elastic.out(1,.35)', delay:.04 });
};
const slip = () => { gsap.to(sun, { x:-6, y:5, rotation:0, duration:.4, ease:DD.ease.soft }); gsap.to(tom, { x:6, y:-4, rotation:0, duration:.4, ease:DD.ease.soft }); };
if(hover){ item.addEventListener('pointerenter', wobble); item.addEventListener('pointerleave', slip); }
item.addEventListener('focusin', wobble); item.addEventListener('focusout', slip);
});
/* 3 — add to cart: sticker flies along an arc to the header cart, toast confirms */
const toast = root.querySelector('.dp-toast'), toastT = toast.querySelector('.dp-toast-t');
const eur = n => '€' + n.toFixed(2);
let toastTl, toastTimer;
const showToast = msg => {
toastT.textContent = msg;
clearTimeout(toastTimer); if(toastTl) toastTl.kill();
if(R){ gsap.set(toast, { y:0, visibility:'visible' }); }
else { toastTl = gsap.fromTo(toast, { y:toast.offsetHeight + 40, autoAlpha:1 }, { y:0, duration:.55, ease:DD.ease.out }); }
toastTimer = setTimeout(() => {
if(R) return gsap.set(toast, { visibility:'hidden' });
toastTl = gsap.to(toast, { y:toast.offsetHeight + 40, duration:.45, ease:'power3.in', onComplete:() => gsap.set(toast, { visibility:'hidden' }) });
}, 2800);
};
const cartPoint = () => {
const t = document.querySelector('[data-dd-cart]');
if(t){ const r = t.getBoundingClientRect();
if(r.width && r.bottom > 0 && r.top < innerHeight) return { x:r.left + r.width / 2, y:r.top + r.height / 2, el:t }; }
return { x:innerWidth - 36, y:30, el:null };
};
const fly = btn => {
const b = btn.getBoundingClientRect(), s = { x:b.left + b.width / 2, y:b.top + b.height / 2 }, e = cartPoint();
const c = { x:(s.x + e.x) / 2 + (e.x < s.x ? 60 : -60), y:Math.min(s.y, e.y) - Math.max(140, Math.abs(s.x - e.x) * .25) };
const dot = document.createElement('div'); dot.className = 'dp-fly'; dot.setAttribute('aria-hidden','true'); dot.textContent = '+1';
document.body.appendChild(dot);
const p = { t:0 };
const place = () => { const t = p.t, u = 1 - t;
gsap.set(dot, { x:u*u*s.x + 2*u*t*c.x + t*t*e.x, y:u*u*s.y + 2*u*t*c.y + t*t*e.y, rotation:t * 540, scale:Math.min(1, t * 9 + .2) * (1 + Math.sin(t * Math.PI) * .35 - t * .45) }); };
place();
gsap.timeline({ onComplete:() => dot.remove() })
.to(p, { t:1, duration:.9, ease:'power2.inOut', onUpdate:place })
.to(dot, { autoAlpha:0, duration:.15 }, '-=.1');
};
root.querySelectorAll('.dp-add:not(:disabled)').forEach(btn => {
const item = btn.closest('.dp-item'), lab = btn.querySelector('.dp-add-l');
const name = item.dataset.name, price = +item.dataset.price;
btn.addEventListener('click', () => {
DD.cart.add({ name, price });
if(!R) fly(btn);
clearTimeout(btn._t);
btn.classList.add('is-done'); lab.textContent = 'Added ✓';
if(!R) gsap.fromTo(lab, { yPercent:100 }, { yPercent:0, duration:.3, ease:'steps(3)' });
btn._t = setTimeout(() => {
btn.classList.remove('is-done'); lab.textContent = 'Add +';
if(!R) gsap.fromTo(lab, { yPercent:-100 }, { yPercent:0, duration:.3, ease:'steps(3)' });
}, 1600);
showToast('Added: ' + name + ' — cart ' + eur(DD.cart.total));
});
});
/* downloads are placeholders — don't jump to top */
root.querySelectorAll('.dp-dl a[href="#"]').forEach(a => a.addEventListener('click', ev => ev.preventDefault()));
/* 4 — scroll-in: cells feed in like paper, stepped clip wipe */
if(!R){
const cells = root.querySelectorAll('.dp-item');
gsap.set(cells, { clipPath:'inset(0% 0% 100% 0%)' });
ScrollTrigger.batch(cells, { start:'top 88%', once:true,
onEnter:b => gsap.to(b, { clipPath:'inset(0% 0% 0% 0%)', duration:.9, ease:'steps(6)', stagger:.14, clearProps:'clipPath' }) });
DD.reveal(root.querySelector('.dp-statement'), { mode:'words' });
gsap.from(root.querySelector('.dp-dl-list').children, { xPercent:-4, autoAlpha:0, duration:.5, ease:'steps(4)', stagger:.1,
scrollTrigger:{ trigger:root.querySelector('.dp-dl'), start:'top 90%', once:true } });
}
DD.refresh();
});
</script>
<section class="dj" id="journal" aria-labelledby="dj-title">
<div class="dj-grid">
<!-- col 1: tomato statement -->
<div class="dj-col dj-col--lead">
<span class="dj-rule" aria-hidden="true"></span>
<div class="dj-lead">
<p class="px dj-lead__kicker"><span class="dot dot--sun dj-blink" aria-hidden="true"></span> 05 / Journal — ink still wet</p>
<h2 class="statement dj-statement" id="dj-title">Studio notes, ink disasters and behind-the-scenes from the press.</h2>
<p class="dj-lead__sub">Written between print runs, usually while the kettle is on and Kevin is sulking in the corner.</p>
<a class="px dj-archive" href="#journal">Read the whole archive <span aria-hidden="true">→</span></a>
</div>
</div>
<!-- col 2: riso image + journal list -->
<div class="dj-col dj-col--list">
<span class="dj-rule" aria-hidden="true"></span>
<figure class="dj-riso" aria-label="Hands holding a fan of colourful printed paper">
<div class="dj-riso__move">
<div class="dj-riso__l dj-riso__l--s"><img src="assets/j2.jpg" alt="Hands holding a fan of colourful printed paper" loading="lazy" width="1200" height="800"></div>
<div class="dj-riso__l dj-riso__l--t" aria-hidden="true"><img src="assets/j2.jpg" alt="" loading="lazy" width="1200" height="800"></div>
</div>
<figcaption class="px dj-riso__cap">Fig. 12 — folding day, round two</figcaption>
</figure>
<div class="dj-head"><span class="px">Journal</span><span class="px dj-mute">5 entries</span></div>
<ul class="dj-rows" role="list">
<li><a class="dj-row" href="#journal"><span class="dj-row__bar" aria-hidden="true"></span><span class="px dj-pill">Note</span><span class="dj-row__t">Why tomato and sunflower live in the Beast</span><time class="px dj-row__d" datetime="2026-09-12">12/09</time></a></li>
<li><a class="dj-row" href="#journal"><span class="dj-row__bar" aria-hidden="true"></span><span class="px dj-pill">Note</span><span class="dj-row__t">We ran out of paper (again)</span><time class="px dj-row__d" datetime="2026-08-28">28/08</time></a></li>
<li><a class="dj-row" href="#journal"><span class="dj-row__bar" aria-hidden="true"></span><span class="px dj-pill dj-pill--how">How-to</span><span class="dj-row__t">Workshop: your first two-colour poster</span><time class="px dj-row__d" datetime="2026-08-03">03/08</time></a></li>
<li><a class="dj-row" href="#journal"><span class="dj-row__bar" aria-hidden="true"></span><span class="px dj-pill">Note</span><span class="dj-row__t">Kevin gets a new drum</span><time class="px dj-row__d" datetime="2026-07-17">17/07</time></a></li>
<li><a class="dj-row" href="#journal"><span class="dj-row__bar" aria-hidden="true"></span><span class="px dj-pill dj-pill--how">How-to</span><span class="dj-row__t">Zine fair season: survival notes</span><time class="px dj-row__d" datetime="2026-06-30">30/06</time></a></li>
</ul>
<p class="px dj-foot"><span>New notes every other Friday</span><a class="link" href="#contact">Get them by post</a></p>
</div>
<!-- col 3: workshop diary + radio -->
<div class="dj-col dj-col--diary">
<span class="dj-rule" aria-hidden="true"></span>
<div class="dj-head"><span class="px">Workshop diary</span><span class="px dj-mute">€45 · 8 seats</span></div>
<ul class="dj-rows" role="list">
<li class="dj-ws">
<div class="dj-ws__top"><time class="px" datetime="2026-10-01">Thu 01 Oct</time><span class="px dj-ws__left dj-ws__left--hot"><span class="dot dj-blink" aria-hidden="true"></span> 2/8 left</span></div>
<p class="dj-ws__t">Your first two-colour poster</p>
<div class="dj-seats" role="img" aria-label="6 of 8 seats booked, 2 left"><i class="is-b"></i><i class="is-b"></i><i class="is-b"></i><i class="is-b"></i><i class="is-b"></i><i class="is-b"></i><i></i><i></i></div>
</li>
<li class="dj-ws">
<div class="dj-ws__top"><time class="px" datetime="2026-11-05">Thu 05 Nov</time><span class="px dj-ws__left">5/8 left</span></div>
<p class="dj-ws__t">Zine in a day: fold, staple, brag</p>
<div class="dj-seats" role="img" aria-label="3 of 8 seats booked, 5 left"><i class="is-b"></i><i class="is-b"></i><i class="is-b"></i><i></i><i></i><i></i><i></i><i></i></div>
</li>
<li class="dj-ws">
<div class="dj-ws__top"><time class="px" datetime="2026-12-03">Thu 03 Dec</time><span class="px dj-ws__left">8/8 left</span></div>
<p class="dj-ws__t">Riso holiday cards (no glitter)</p>
<div class="dj-seats" role="img" aria-label="0 of 8 seats booked, 8 left"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
</li>
<li class="dj-ws">
<div class="dj-ws__top"><time class="px" datetime="2027-01-07">Thu 07 Jan</time><span class="px dj-ws__left">8/8 left</span></div>
<p class="dj-ws__t">Stencils, overprints and happy accidents</p>
<div class="dj-seats" role="img" aria-label="0 of 8 seats booked, 8 left"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
</li>
</ul>
<p class="px dj-legend"><span class="dj-key dj-key--b" aria-hidden="true"></span> booked <span class="dj-key" aria-hidden="true"></span> open · <a class="link" href="#studio">How it works</a></p>
<div class="dj-head dj-head--gap"><span class="px">Radio / podcast</span><span class="px dj-mute">On air</span></div>
<ul class="dj-rows" role="list">
<li class="dj-air">
<time class="px dj-air__d" datetime="2026-10-09">09/10</time>
<div><p class="dj-air__t">Kade Film Night live — posters vs. trailers</p><p class="px dj-mute"><span class="dot dj-blink" aria-hidden="true"></span> Upcoming · live Q&A</p></div>
</li>
<li class="dj-air">
<time class="px dj-air__d" datetime="2026-09-04">04/09</time>
<div><p class="dj-air__t">Maas Radio Night Shift — Sem plays records we printed sleeves for</p><p class="px dj-mute">Radio · 2 hrs</p></div>
</li>
<li class="dj-air">
<time class="px dj-air__d" datetime="2026-08-19">19/08</time>
<div><p class="dj-air__t">Paper Jam podcast ep. 58 — the Beast, explained slowly</p><p class="px dj-mute">Podcast · 54 min</p></div>
</li>
</ul>
</div>
<!-- col 4: clippings -->
<div class="dj-col dj-col--clips">
<div class="dj-head"><span class="px">Clippings</span><span class="px dj-mute">Press about the press</span></div>
<ul class="dj-rows" role="list">
<li class="dj-clip">
<p class="px dj-clip__src">Paperweight Magazine</p>
<a class="dj-clip__t" href="#journal">The two-person press that prints louder than it talks</a>
<p class="dj-clip__s">A long afternoon on Noordereiland, three misprints and one very honest opinion about neon pink.</p>
<p class="px dj-clip__meta"><time datetime="2026-08-21">21/08/26</time><span class="dj-tag">Feature</span></p>
</li>
<li class="dj-clip">
<p class="px dj-clip__src">Swatch Quarterly</p>
<div class="dj-clip__img"><img src="assets/j1.jpg" alt="Fanned-out colour paper swatches in bright tones" loading="lazy" width="1200" height="800"></div>
<a class="dj-clip__t" href="#journal">Twelve inks, one kettle: inside the colour cabinet</a>
<p class="dj-clip__s">Why the studio's paper drawer is sorted by mood instead of weight.</p>
<p class="px dj-clip__meta"><time datetime="2026-06-12">12/06/26</time><span class="dj-tag">Photo essay</span></p>
</li>
<li class="dj-clip">
<p class="px dj-clip__src">Noord Nieuws</p>
<a class="dj-clip__t" href="#journal">Island studio opens its doors every first Thursday</a>
<p class="dj-clip__s">Neighbours report inky fingers, big grins and a suspicious number of tote bags.</p>
<p class="px dj-clip__meta"><time datetime="2026-04-02">02/04/26</time><span class="dj-tag">Local</span></p>
</li>
</ul>
</div>
</div>
</section>
<style>
.dj{scroll-margin-top:80px;border-bottom:var(--hair)}
.dj-grid{display:grid;grid-template-columns:1.05fr 1.35fr 1fr 1fr;align-items:stretch}
.dj-col{position:relative;min-width:0;display:flex;flex-direction:column}
.dj-rule{position:absolute;top:0;right:0;bottom:0;width:1px;background:var(--line);transform-origin:50% 0;z-index:2;pointer-events:none}
.dj-mute{color:var(--mute)}
.dj-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:12px clamp(14px,1.6vw,22px);border-bottom:var(--hair)}
.dj-head--gap{border-top:var(--hair);margin-top:auto}
.dj-rows{list-style:none;margin:0;padding:0}
.dj-rows>li+li{border-top:var(--hair)}
/* col 1 */
.dj-col--lead{background:var(--tomato);color:#fff}
.dj-lead{display:flex;flex-direction:column;gap:18px;height:100%;padding:clamp(18px,2vw,28px) clamp(16px,1.8vw,26px)}
.dj-lead__kicker{margin:0;display:flex;align-items:center;gap:8px}
.dj-statement{font-size:clamp(28px,2.7vw,44px)}
.dj-lead__sub{margin:0;max-width:30ch;color:rgba(255,255,255,.88)}
.dj-archive{margin-top:auto;display:inline-flex;align-items:center;gap:.6em;align-self:flex-start;padding:10px 0;color:#fff;text-decoration:none;font-size:12px;border-bottom:1px solid rgba(255,255,255,.7)}
.dj-archive span{transition:transform .35s var(--ease)}
.dj-archive:hover span,.dj-archive:focus-visible span{transform:translateX(6px)}
.dj-archive:focus-visible{outline-color:#fff}
/* col 2: riso print */
.dj-riso{position:relative;margin:0;aspect-ratio:3/2;overflow:hidden;border-bottom:var(--hair);background:var(--bg)}
.dj-riso__move{position:absolute;left:0;right:0;top:-9%;bottom:-9%}
.dj-riso__l{position:absolute;inset:0;background:var(--bg);isolation:isolate}
.dj-riso__l img{width:100%;height:100%;object-fit:cover}
.dj-riso__l::after{content:"";position:absolute;inset:0;mix-blend-mode:screen;pointer-events:none}
.dj-riso__l--s img{filter:none}
.dj-riso__l--s::after{display:none}
.dj-riso__l--t{mix-blend-mode:multiply}
.dj-riso__l--t{display:none}
.dj-riso__l--t::after{background:var(--tomato)}
.dj-riso__cap{position:absolute;left:10px;bottom:10px;margin:0;padding:4px 7px;background:var(--bg);border:var(--hair);font-size:10px;z-index:1}
/* journal rows */
.dj-row{position:relative;display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;padding:14px clamp(14px,1.6vw,22px);text-decoration:none;color:var(--ink);overflow:hidden;outline-offset:-3px}
.dj-row>*:not(.dj-row__bar){position:relative;z-index:1}
.dj-row__bar{position:absolute;inset:0;background:var(--sun);transform:scaleX(0);transform-origin:0 50%;transition:transform .5s var(--ease)}
.dj-row__t{font-weight:600;line-height:1.25;transition:transform .45s var(--ease)}
.dj-row__d{white-space:nowrap}
.dj-row:hover .dj-row__bar,.dj-row:focus-visible .dj-row__bar{transform:scaleX(1)}
.dj-row:hover .dj-row__t,.dj-row:focus-visible .dj-row__t{transform:translateX(8px)}
.dj-pill{display:inline-block;min-width:64px;text-align:center;padding:3px 8px;border:var(--hair);border-radius:999px;font-size:10px;background:var(--bg)}
.dj-pill--how{background:var(--tomato-soft)}
.dj-foot{margin:auto 0 0;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px clamp(14px,1.6vw,22px);border-top:var(--hair);color:var(--mute);font-size:10px}
.dj-foot a{color:var(--ink)}
/* col 3: workshops */
.dj-ws{padding:14px clamp(14px,1.6vw,22px)}
.dj-ws__top{display:flex;justify-content:space-between;gap:10px}
.dj-ws__left--hot{color:var(--tomato-deep);display:inline-flex;align-items:center;gap:6px}
.dj-ws__t{margin:6px 0 10px;font-weight:600;line-height:1.25}
.dj-seats{display:grid;grid-template-columns:repeat(8,1fr);gap:3px;height:10px}
.dj-seats i{display:block;border:var(--hair);background:var(--sun);transform-origin:0 50%}
.dj-seats i.is-b{background:var(--tomato)}
.dj-legend{margin:0;padding:10px clamp(14px,1.6vw,22px) 14px;border-top:var(--hair);color:var(--mute);font-size:10px}
.dj-legend a{color:var(--ink)}
.dj-key{display:inline-block;width:9px;height:9px;border:var(--hair);background:var(--sun);vertical-align:-1px;margin-left:4px}
.dj-key--b{background:var(--tomato);margin-left:0}
.dj-air{display:grid;grid-template-columns:auto 1fr;gap:12px;padding:12px clamp(14px,1.6vw,22px)}
.dj-air p{margin:0}
.dj-air__d{padding-top:3px}
.dj-air__t{font-weight:500;line-height:1.3;margin-bottom:4px !important}
.dj-air .dot{width:6px;height:6px}
/* col 4: clippings */
.dj-col--clips{background:var(--grey)}
.dj-clip{padding:16px clamp(14px,1.6vw,22px) 18px;display:flex;flex-direction:column;gap:8px}
.dj-clip p{margin:0}
.dj-clip__src{color:var(--tomato-deep)}
.dj-clip__t{font-weight:600;font-size:17px;line-height:1.2;letter-spacing:-.01em;text-decoration:none}
.dj-clip__t:hover,.dj-clip__t:focus-visible{text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}
.dj-clip__s{color:var(--mute);font-size:14px;line-height:1.45}
.dj-clip__meta{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:10px}
.dj-tag{padding:2px 7px;border:var(--hair);border-radius:999px;background:var(--bg)}
.dj-clip__img{aspect-ratio:16/9;overflow:hidden;border:var(--hair)}
.dj-clip__img img{width:100%;height:100%;object-fit:cover;transition:transform .8s var(--ease)}
.dj-clip:hover .dj-clip__img img{transform:scale(1.05)}
/* blink (only runs while on screen) */
.dj-blink{animation:dj-blink 1.2s steps(1) infinite;animation-play-state:paused}
.dj.is-on .dj-blink{animation-play-state:running}
@keyframes dj-blink{50%{opacity:0}}
/* tablet: 2 x 2 */
@media (max-width:1100px){
.dj-grid{grid-template-columns:1fr 1fr}
.dj-col--lead,.dj-col--list{border-bottom:var(--hair)}
.dj-col--list .dj-rule,.dj-col--diary .dj-rule{display:none}
.dj-col--diary{border-right:var(--hair)}
.dj-lead{position:static}
}
/* mobile: stack */
@media (max-width:700px){
.dj-grid{grid-template-columns:1fr}
.dj-rule{display:none}
.dj-col--diary{border-right:0;border-bottom:var(--hair)}
.dj-lead{min-height:340px;padding:24px 16px}
.dj-head,.dj-row,.dj-ws,.dj-air,.dj-clip,.dj-legend,.dj-foot{padding-left:16px;padding-right:16px}
.dj-row{grid-template-columns:1fr auto;row-gap:6px}
.dj-row .dj-pill{grid-column:1 / -1;justify-self:start}
.dj-row__t{grid-column:1}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.dj-blink{animation:none}
.dj-row__bar,.dj-row__t,.dj-archive span,.dj-clip__img img{transition:none}
}
</style>
<script>
DD.later(() => {
const root = document.getElementById('journal');
if(!root) return;
// blink only while visible
new IntersectionObserver(e => root.classList.toggle('is-on', e[0].isIntersecting)).observe(root);
const statement = root.querySelector('.dj-statement');
const riso = root.querySelector('.dj-riso');
const layS = root.querySelector('.dj-riso__l--s');
const layT = root.querySelector('.dj-riso__l--t');
if(DD.reduce){ DD.split(statement, 'words'); return; }
DD.reveal(statement, { mode:'words', start:'top 85%' });
// column hairlines draw in
const rules = root.querySelectorAll('.dj-rule');
gsap.fromTo(rules, { scaleY:0 }, { scaleY:1, duration:1.4, ease:'power3.inOut', stagger:.14,
scrollTrigger:{ trigger:root, start:'top 80%', once:true } });
// rows + cards stagger in (y + clip), per list
root.querySelectorAll('.dj-rows').forEach(list => {
const items = list.children;
gsap.set(items, { y:26, clipPath:'inset(0% 0% 100% 0%)' });
gsap.to(items, { y:0, clipPath:'inset(0% 0% 0% 0%)', duration:.9, ease:DD.ease.out, stagger:.08,
clearProps:'clipPath',
scrollTrigger:{ trigger:list, start:'top 88%', once:true } });
});
// seat bars fill
root.querySelectorAll('.dj-seats').forEach(bar => {
gsap.fromTo(bar.children, { scaleX:0 }, { scaleX:1, duration:.5, ease:'power2.out', stagger:.05,
scrollTrigger:{ trigger:bar, start:'top 90%', once:true } });
});
// riso: misregistered -> register on scroll-in
gsap.set(layS, { x:-9, y:6 }); gsap.set(layT, { x:9, y:-5 });
gsap.to([layS, layT], { x:0, y:0, duration:1.3, ease:'elastic.out(1,.6)', delay:.15,
scrollTrigger:{ trigger:riso, start:'top 80%', once:true } });
// hover: slip out of register then snap back
riso.addEventListener('pointerenter', () => {
gsap.timeline()
.to(layS, { x:-6, y:4, duration:.18, ease:'power2.out' }, 0)
.to(layT, { x:6, y:-3, duration:.18, ease:'power2.out' }, 0)
.to([layS, layT], { x:0, y:0, duration:.7, ease:'elastic.out(1,.5)' }, .2);
});
// slow scrubbed parallax
gsap.fromTo(root.querySelector('.dj-riso__move'), { yPercent:-6 }, { yPercent:6, ease:'none',
scrollTrigger:{ trigger:riso, start:'top bottom', end:'bottom top', scrub:.6 } });
DD.refresh();
});
</script>
<footer class="df" id="contact" aria-labelledby="df-title">
<!-- ============ 1. STICKER WALL ============ -->
<div class="df-band" role="region" aria-label="Sticker wall: drag the stickers around, or shuffle them">
<div class="df-bhead">
<p class="px df-blabel">(06) The sticker wall</p>
<p class="px df-bhint" aria-hidden="true">Grab · toss · stick</p>
<button class="df-pxbtn df-shuffle" type="button">Shuffle <span aria-hidden="true">↻</span></button>
</div>
<p class="big df-bgword" aria-hidden="true"><span>Stick</span> <span>around</span></p>
<div class="df-stage">
<!-- 1 PRINT LOUD round badge -->
<div class="df-st" style="--w:190" data-h=".09,.34,-8" data-mh=".27,.21,-8" role="img" aria-label="Round sticker: Print loud">
<div class="df-fl"><svg viewBox="0 0 200 200" aria-hidden="true">
<circle cx="100" cy="100" r="96" class="df-w df-o"/>
<circle cx="100" cy="100" r="84" class="df-r df-o"/>
<path id="df-c1" class="df-n" d="M100,100 m-63,0 a63,63 0 1,1 126,0 a63,63 0 1,1 -126,0"/>
<text class="df-td df-w" font-size="23"><textPath href="#df-c1" textLength="392" lengthAdjust="spacing">PRINT LOUD • PRINT LOUD • PRINT LOUD •</textPath></text>
<circle cx="100" cy="100" r="40" class="df-y df-o"/>
<rect x="72" y="84" width="56" height="22" rx="11" class="df-w df-o"/>
<path d="M128,95 h6 v18 h-34 v16" class="df-n df-o" style="stroke-width:4"/>
<rect x="94" y="126" width="12" height="16" rx="3" class="df-k"/>
</svg></div>
</div>
<!-- 2 KEVIN LIVES -->
<div class="df-st" style="--w:170" data-h=".42,.3,9" data-mh=".74,.23,10" role="img" aria-label="Round sticker: Kevin lives, with a smiling one-drum printer">
<div class="df-fl"><svg viewBox="0 0 200 200" aria-hidden="true">
<circle cx="100" cy="100" r="96" class="df-w df-o"/>
<circle cx="100" cy="100" r="84" class="df-p df-o"/>
<circle cx="100" cy="100" r="50" class="df-w df-o"/>
<path id="df-c2" class="df-n" d="M100,100 m-67,0 a67,67 0 1,1 134,0 a67,67 0 1,1 -134,0"/>
<text class="df-td df-k" font-size="22"><textPath href="#df-c2" textLength="418" lengthAdjust="spacing">KEVIN LIVES • KEVIN LIVES • KEVIN LIVES •</textPath></text>
<rect x="68" y="86" width="64" height="40" rx="7" class="df-y df-o"/>
<circle cx="100" cy="80" r="15" class="df-r df-o"/>
<circle cx="100" cy="80" r="5" class="df-w df-o" style="stroke-width:2"/>
<circle cx="87" cy="103" r="3.5" class="df-k"/><circle cx="113" cy="103" r="3.5" class="df-k"/>
<path d="M89,113 q11,9 22,0" class="df-n df-o"/>
<rect x="76" y="126" width="48" height="8" class="df-w df-o" style="stroke-width:2"/>
</svg></div>
</div>
<!-- 3 2 DRUMS 1 DREAM scalloped -->
<div class="df-st" style="--w:180" data-h=".74,.3,-10" data-mh=".27,.44,-12" role="img" aria-label="Scalloped sticker: 2 drums, 1 dream">
<div class="df-fl"><svg viewBox="0 0 200 200" aria-hidden="true">
<circle cx="100" cy="100" r="88" class="df-k"/>
<circle cx="100" cy="100" r="88" class="df-n" style="stroke:var(--ink);stroke-width:21;stroke-linecap:round;stroke-dasharray:.01 18.43"/>
<circle cx="100" cy="100" r="86" class="df-y"/>
<circle cx="100" cy="100" r="88" class="df-n" style="stroke:var(--sun-deep);stroke-width:15;stroke-linecap:round;stroke-dasharray:.01 18.43"/>
<circle cx="100" cy="100" r="70" class="df-n df-o" style="stroke-width:2;stroke-dasharray:4 5"/>
<text x="100" y="82" text-anchor="middle" class="df-td df-k" font-size="36" textLength="116" lengthAdjust="spacingAndGlyphs">2 DRUMS</text>
<rect x="34" y="92" width="132" height="44" class="df-w df-o"/>
<text x="100" y="128" text-anchor="middle" class="df-td df-r" font-size="36" textLength="112" lengthAdjust="spacingAndGlyphs">1 DREAM</text>
<text x="100" y="158" text-anchor="middle" class="df-tp df-k" font-size="11">ROTTERDAM RISO</text>
</svg></div>
</div>
<!-- 4 INK ON HANDS -->
<div class="df-st" style="--w:160" data-h=".07,.8,-10" data-mh=".74,.46,8" role="img" aria-label="Round sticker: Ink on hands, with a tomato hand print">
<div class="df-fl"><svg viewBox="0 0 200 200" aria-hidden="true">
<defs><g id="df-hand">
<rect x="76" y="92" width="48" height="46" rx="18"/>
<rect x="77" y="64" width="11" height="42" rx="5.5"/>
<rect x="90" y="56" width="11" height="48" rx="5.5"/>
<rect x="103" y="58" width="11" height="46" rx="5.5"/>
<rect x="115" y="68" width="10" height="38" rx="5"/>
<rect x="58" y="96" width="11" height="36" rx="5.5" transform="rotate(-38 64 114)"/>
</g></defs>
<circle cx="100" cy="100" r="96" class="df-w df-o"/>
<circle cx="100" cy="100" r="84" class="df-p df-o"/>
<path id="df-c3" class="df-n" d="M100,100 m-66,0 a66,66 0 1,1 132,0 a66,66 0 1,1 -132,0"/>
<text class="df-td df-k" font-size="22"><textPath href="#df-c3" textLength="412" lengthAdjust="spacing">INK ON HANDS • INK ON HANDS • INK ON HANDS •</textPath></text>
<use href="#df-hand" style="fill:var(--ink);stroke:var(--ink);stroke-width:6;stroke-linejoin:round"/>
<use href="#df-hand" style="fill:var(--tomato)"/>
<circle cx="112" cy="116" r="4" class="df-p"/><circle cx="90" cy="122" r="2.5" class="df-p"/>
</svg></div>
</div>
<!-- 5 starburst €6! -->
<div class="df-st" style="--w:150" data-h=".93,.55,15" data-mh=".25,.66,14" role="img" aria-label="Starburst sticker: sticker sheet, 6 euro">
<div class="df-fl"><svg viewBox="0 0 160 160" aria-hidden="true">
<polygon points="80.0,8.0 89.7,24.9 104.6,12.3 108.0,31.5 126.3,24.8 122.9,44.0 142.4,44.0 132.6,60.8 150.9,67.5 136.0,80.0 150.9,92.5 132.6,99.2 142.4,116.0 122.9,116.0 126.3,135.2 108.0,128.5 104.6,147.7 89.7,135.1 80.0,152.0 70.3,135.1 55.4,147.7 52.0,128.5 33.7,135.2 37.1,116.0 17.6,116.0 27.4,99.2 9.1,92.5 24.0,80.0 9.1,67.5 27.4,60.8 17.6,44.0 37.1,44.0 33.7,24.8 52.0,31.5 55.4,12.3 70.3,24.9" style="fill:var(--bg);stroke:var(--ink);stroke-width:14;stroke-linejoin:round"/>
<polygon points="80.0,8.0 89.7,24.9 104.6,12.3 108.0,31.5 126.3,24.8 122.9,44.0 142.4,44.0 132.6,60.8 150.9,67.5 136.0,80.0 150.9,92.5 132.6,99.2 142.4,116.0 122.9,116.0 126.3,135.2 108.0,128.5 104.6,147.7 89.7,135.1 80.0,152.0 70.3,135.1 55.4,147.7 52.0,128.5 33.7,135.2 37.1,116.0 17.6,116.0 27.4,99.2 9.1,92.5 24.0,80.0 9.1,67.5 27.4,60.8 17.6,44.0 37.1,44.0 33.7,24.8 52.0,31.5 55.4,12.3 70.3,24.9" style="fill:var(--bg);stroke:var(--bg);stroke-width:9;stroke-linejoin:round"/>
<polygon points="80.0,8.0 89.7,24.9 104.6,12.3 108.0,31.5 126.3,24.8 122.9,44.0 142.4,44.0 132.6,60.8 150.9,67.5 136.0,80.0 150.9,92.5 132.6,99.2 142.4,116.0 122.9,116.0 126.3,135.2 108.0,128.5 104.6,147.7 89.7,135.1 80.0,152.0 70.3,135.1 55.4,147.7 52.0,128.5 33.7,135.2 37.1,116.0 17.6,116.0 27.4,99.2 9.1,92.5 24.0,80.0 9.1,67.5 27.4,60.8 17.6,44.0 37.1,44.0 33.7,24.8 52.0,31.5 55.4,12.3 70.3,24.9" class="df-r df-o"/>
<text x="80" y="60" text-anchor="middle" class="df-tp df-w" font-size="10">ONLY</text>
<text x="80" y="102" text-anchor="middle" class="df-td df-w" font-size="46">€6!</text>
<text x="80" y="118" text-anchor="middle" class="df-tp df-w" font-size="8">PER SHEET</text>
</svg></div>
</div>
<!-- 6 THE BEAST drum machine -->
<div class="df-st" style="--w:220" data-h=".62,.56,-4" data-mh=".7,.67,-5" role="img" aria-label="Sticker: the Beast, a two-drum risograph with tomato and sunflower drums">
<div class="df-fl"><svg viewBox="0 0 220 170" aria-hidden="true">
<rect x="4" y="4" width="212" height="162" rx="20" class="df-w df-o"/>
<text x="110" y="30" text-anchor="middle" class="df-tp df-k" font-size="10">2-DRUM · EST. OLD</text>
<rect x="26" y="40" width="168" height="78" rx="8" class="df-p df-o"/>
<circle cx="80" cy="79" r="26" class="df-r df-o"/><circle cx="80" cy="79" r="9" class="df-w df-o" style="stroke-width:2"/>
<circle cx="140" cy="79" r="26" class="df-y df-o"/><circle cx="140" cy="79" r="9" class="df-w df-o" style="stroke-width:2"/>
<path d="M60,79 a20,20 0 0 1 20,-20 M120,79 a20,20 0 0 1 20,-20" class="df-n" style="stroke:var(--bg);stroke-width:3;stroke-linecap:round"/>
<rect x="176" y="48" width="10" height="10" class="df-k"/>
<path d="M50,118 h120 l14,18 h-148 z" class="df-w df-o"/>
<text x="110" y="156" text-anchor="middle" class="df-td df-k" font-size="20">THE BEAST</text>
</svg></div>
</div>
<!-- 7 tape: wet ink -->
<div class="df-st" style="--w:240" data-h=".57,.2,-14" data-mh=".52,.33,-12" role="img" aria-label="Tape strip: wet ink, hands off">
<div class="df-fl"><svg viewBox="0 0 240 56" aria-hidden="true">
<path d="M10,6 H230 L236,14 L228,22 L236,30 L228,38 L236,46 L230,50 H10 L4,42 L12,34 L4,26 L12,18 L4,10 Z" class="df-p df-o" style="stroke-width:2;fill-opacity:.94"/>
<path d="M22,14 H218 M22,42 H218" class="df-n" style="stroke:var(--tomato);stroke-width:1.5;stroke-dasharray:3 4"/>
<text x="120" y="33" text-anchor="middle" class="df-tp df-k" font-size="12">WET INK · HANDS OFF</text>
</svg></div>
</div>
<!-- 8 tape: fragile zines -->
<div class="df-st" style="--w:200" data-h=".87,.84,20" role="img" aria-label="Tape strip: fragile zines">
<div class="df-fl"><svg viewBox="0 0 200 50" aria-hidden="true">
<path d="M8,5 H192 L196,11 L190,17 L196,25 L190,33 L196,40 L192,45 H8 L4,39 L10,32 L4,25 L10,18 L4,11 Z" class="df-y df-o" style="stroke-width:2"/>
<text x="100" y="33" text-anchor="middle" class="df-td df-k" font-size="22">FRAGILE ZINES</text>
</svg></div>
</div>
<!-- 9 NOW BOOKING ticket -->
<div class="df-st" style="--w:280" data-h=".3,.56,-5" data-mh=".33,.85,-4" role="img" aria-label="Ticket sticker: now booking workshops, first Thursdays, 45 euro, 8 seats">
<div class="df-fl"><svg viewBox="0 0 280 124" aria-hidden="true">
<path d="M8,8 H272 V50 A12,12 0 0 0 272,74 V116 H8 V74 A12,12 0 0 0 8,50 Z" class="df-w"/>
<path d="M8,8 H72 V116 H8 V74 A12,12 0 0 0 8,50 Z" class="df-r"/>
<path d="M8,8 H272 V50 A12,12 0 0 0 272,74 V116 H8 V74 A12,12 0 0 0 8,50 Z" class="df-n df-o"/>
<path d="M72,12 V112" class="df-n" style="stroke:var(--ink);stroke-width:2;stroke-dasharray:4 4"/>
<text transform="translate(46 62) rotate(-90)" text-anchor="middle" class="df-tp df-w" font-size="12">Nº 045</text>
<text x="86" y="34" class="df-tp df-r" font-size="10">ADMIT ONE · INKY HANDS</text>
<text x="84" y="76" class="df-td df-k" font-size="40" textLength="174" lengthAdjust="spacingAndGlyphs">NOW BOOKING</text>
<path d="M86,86 H258" class="df-n df-o" style="stroke-width:1.5"/>
<text x="86" y="104" class="df-tp df-k" font-size="10">WORKSHOPS · 1ST THU · €45</text>
</svg></div>
</div>
<!-- 10 HANDLE WITH INK label (HTML) -->
<div class="df-st" style="--w:190" data-h=".55,.84,5" role="img" aria-label="Label sticker: printed matter, handle with ink">
<div class="df-fl"><div class="df-lab df-lab--a" aria-hidden="true">
<div class="df-lab__in"><span class="df-lab__bar">Printed matter</span><b>Handle with ink</b><span class="df-lab__row"><i></i><i></i><i></i><i></i></span></div>
</div></div>
</div>
<!-- 11 DOUBLE DRUM PRESS label (HTML) -->
<div class="df-st" style="--w:230" data-h=".26,.24,-4" role="img" aria-label="Label sticker: Double Drum Press, Rotterdam, since 2019">
<div class="df-fl"><div class="df-lab df-lab--b" aria-hidden="true">
<div class="df-lab__in"><b>Double Drum Press</b><span class="df-lab__sub">Rotterdam · riso · since 2019</span></div>
</div></div>
</div>
<!-- 12 speech bubble -->
<div class="df-st" style="--w:220" data-h=".12,.58,6" data-mh=".73,.87,6" role="img" aria-label="Speech bubble sticker: misregistered and proud">
<div class="df-fl"><svg viewBox="0 0 220 140" aria-hidden="true">
<path d="M26,6 H194 Q214,6 214,26 V84 Q214,104 194,104 H84 L48,134 L58,104 H26 Q6,104 6,84 V26 Q6,6 26,6 Z" class="df-w df-o"/>
<text x="113" y="55" text-anchor="middle" class="df-td df-r df-mis" font-size="32" textLength="180" lengthAdjust="spacingAndGlyphs">MISREGISTERED</text>
<text x="110" y="52" text-anchor="middle" class="df-td df-k df-mis" font-size="32" textLength="180" lengthAdjust="spacingAndGlyphs">MISREGISTERED</text>
<text x="110" y="84" text-anchor="middle" class="df-tp df-k" font-size="13">& PROUD OF IT</text>
</svg></div>
</div>
<!-- 13 bolt -->
<div class="df-st" style="--w:100" data-h=".4,.8,-18" role="img" aria-label="Lightning bolt sticker">
<div class="df-fl"><svg viewBox="0 0 110 170" aria-hidden="true">
<defs><path id="df-bolt" d="M64,8 L14,96 L50,96 L38,162 L98,66 L60,66 L78,8 Z"/></defs>
<use href="#df-bolt" style="fill:var(--bg);stroke:var(--ink);stroke-width:16;stroke-linejoin:round"/>
<use href="#df-bolt" style="fill:var(--bg);stroke:var(--bg);stroke-width:11;stroke-linejoin:round"/>
<use href="#df-bolt" style="fill:var(--tomato);stroke:var(--ink);stroke-width:3;stroke-linejoin:round"/>
<path d="M60,26 L36,72" class="df-n" style="stroke:var(--bg);stroke-width:4;stroke-linecap:round"/>
</svg></div>
</div>
<!-- 14 pixel smiley -->
<div class="df-st" style="--w:120" data-h=".47,.58,12" data-mh=".52,.56,12" role="img" aria-label="Pixel smiley sticker">
<div class="df-fl"><svg viewBox="0 0 140 140" aria-hidden="true" shape-rendering="crispEdges">
<rect x="3" y="3" width="134" height="134" rx="14" class="df-w df-o" shape-rendering="geometricPrecision"/>
<polygon points="34,10 106,10 106,22 118,22 118,34 130,34 130,106 118,106 118,118 106,118 106,130 34,130 34,118 22,118 22,106 10,106 10,34 22,34 22,22 34,22" class="df-r df-o"/>
<rect x="34" y="46" width="12" height="24" class="df-k"/><rect x="94" y="46" width="12" height="24" class="df-k"/>
<rect x="22" y="82" width="12" height="12" class="df-k"/><rect x="106" y="82" width="12" height="12" class="df-k"/><rect x="34" y="94" width="72" height="12" class="df-k"/>
<rect x="46" y="22" width="12" height="12" class="df-p"/>
</svg></div>
</div>
<!-- 15 arrow label -->
<div class="df-st" style="--w:250" data-h=".24,.84,3" role="img" aria-label="Arrow sticker: this way to the zines">
<div class="df-fl"><svg viewBox="0 0 260 80" aria-hidden="true">
<path d="M6,8 H204 L254,40 L204,72 H6 L22,40 Z" class="df-p df-o"/>
<circle cx="222" cy="40" r="6" class="df-r df-o" style="stroke-width:2"/>
<text x="112" y="36" text-anchor="middle" class="df-tp df-k" font-size="13">THIS WAY TO</text>
<text x="112" y="58" text-anchor="middle" class="df-td df-k" font-size="22">THE ZINES</text>
</svg></div>
</div>
<!-- 16 EST oval -->
<div class="df-st" style="--w:210" data-h=".79,.6,8" role="img" aria-label="Oval sticker: established 2019, Noordereiland">
<div class="df-fl"><svg viewBox="0 0 220 130" aria-hidden="true">
<ellipse cx="110" cy="65" rx="104" ry="59" class="df-w df-o"/>
<ellipse cx="110" cy="65" rx="92" ry="48" class="df-n df-o" style="stroke-width:1.5"/>
<text x="110" y="45" text-anchor="middle" class="df-tp df-r" font-size="10">ROTTERDAM</text>
<text x="110" y="82" text-anchor="middle" class="df-td df-k" font-size="40">EST. 2019</text>
<text x="110" y="100" text-anchor="middle" class="df-tp df-k" font-size="9">NOORDEREILAND</text>
<circle cx="30" cy="65" r="5" class="df-r df-o" style="stroke-width:1.5"/><circle cx="190" cy="65" r="5" class="df-r df-o" style="stroke-width:1.5"/>
</svg></div>
</div>
<!-- 17 registration mark -->
<div class="df-st" style="--w:110" data-h=".91,.26,0" role="img" aria-label="Registration mark sticker">
<div class="df-fl"><svg viewBox="0 0 120 120" aria-hidden="true">
<circle cx="60" cy="60" r="56" class="df-w df-o"/>
<path d="M60,60 V30 A30,30 0 0 1 90,60 Z M60,60 V90 A30,30 0 0 1 30,60 Z" class="df-r"/>
<circle cx="60" cy="60" r="30" class="df-n df-o"/>
<path d="M60,14 V106 M14,60 H106" class="df-n df-o" style="stroke-width:2"/>
<circle cx="60" cy="60" r="44" class="df-n df-o" style="stroke-width:1.5;stroke-dasharray:3 5"/>
</svg></div>
</div>
<!-- 18 soy ink tag -->
<div class="df-st" style="--w:190" data-h=".7,.84,-8" role="img" aria-label="Tag sticker: 100 percent soy ink">
<div class="df-fl"><svg viewBox="0 0 200 112" aria-hidden="true">
<path d="M52,6 H194 V106 H52 L8,56 Z M36,56 m-7,0 a7,7 0 1,0 14,0 a7,7 0 1,0 -14,0 Z" class="df-y df-o" style="fill-rule:evenodd"/>
<path d="M60,16 H184 V96 H60" class="df-n df-o" style="stroke-width:1.5;stroke-dasharray:3 4"/>
<text x="124" y="62" text-anchor="middle" class="df-td df-k" font-size="44">100%</text>
<text x="124" y="84" text-anchor="middle" class="df-tp df-k" font-size="12">SOY INK</text>
</svg></div>
</div>
</div>
</div>
<!-- ============ 2. STATEMENT / INDEX / CONTACT ============ -->
<div class="df-main">
<div class="cell df-c df-state">
<p class="px df-lbl"><span class="dot dot--sun"></span> Get in touch</p>
<h2 class="big df-big" id="df-title">Riso printing for <span class="df-hl">loud</span> ideas, small runs and big feelings.</h2>
<p class="df-note">Two people, two drums and far too many paper samples on Noordereiland. Bring us a sketch, a deadline or a half-baked zine idea — we'll bring the ink.</p>
</div>
<nav class="cell df-c df-navc" aria-label="Footer">
<p class="px df-lbl">Index</p>
<ul class="df-nav">
<li><a class="df-nl" href="#work"><span class="px df-ni">01</span><span class="df-nt">Work</span><span class="df-na" aria-hidden="true">→</span></a></li>
<li><a class="df-nl" href="#studio"><span class="px df-ni">02</span><span class="df-nt">Studio</span><span class="df-na" aria-hidden="true">→</span></a></li>
<li><a class="df-nl" href="#shop"><span class="px df-ni">03</span><span class="df-nt">Shop</span><span class="df-na" aria-hidden="true">→</span></a></li>
<li><a class="df-nl" href="#journal"><span class="px df-ni">04</span><span class="df-nt">Journal</span><span class="df-na" aria-hidden="true">→</span></a></li>
<li><a class="df-nl" href="#studio"><span class="px df-ni">05</span><span class="df-nt">Workshops</span><span class="df-na" aria-hidden="true">→</span></a></li>
</ul>
</nav>
<div class="cell df-c df-contact">
<p class="px df-lbl">Say hello</p>
<a class="df-mail" href="mailto:[email protected]">[email protected]</a>
<address class="df-addr">
Maaskade 112, Noordereiland<br>Rotterdam, the Netherlands
<span class="px df-hours">Studio open Tue–Sat · 10–18 · knock loudly</span>
</address>
<ul class="df-soc" aria-label="Social links">
<li><a class="px df-sl" href="#contact">Instagram <span aria-hidden="true">↗</span></a></li>
<li><a class="px df-sl" href="#contact">Are.na <span aria-hidden="true">↗</span></a></li>
<li><a class="px df-sl" href="#contact">Mastodon <span aria-hidden="true">↗</span></a></li>
<li><a class="px df-sl" href="#df-email">Newsletter <span aria-hidden="true">↗</span></a></li>
</ul>
<form class="df-form" novalidate aria-labelledby="df-form-h">
<p class="px df-form-h" id="df-form-h">Newsletter — one loud email a month, zero spam</p>
<div class="df-field">
<label class="df-sr" for="df-email">Email address</label>
<input class="df-input" id="df-email" name="email" type="email" inputmode="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="df-msg">
<button class="df-pxbtn df-sub" type="submit">Subscribe</button>
</div>
<p class="px df-msg" id="df-msg" aria-live="polite"></p>
</form>
</div>
</div>
<!-- ============ 3. BOTTOM BAR ============ -->
<div class="df-bar px">
<p class="cell df-b">The Beast <span class="dot df-blink" aria-hidden="true"></span> online</p>
<p class="cell df-b">© 2026 Double Drum Press</p>
<p class="cell df-b"><a class="df-bl" href="#contact">Terms</a> — <a class="df-bl" href="#contact">Privacy</a> — <a class="df-bl" href="#contact">Colophon</a></p>
<p class="cell df-b df-b--top"><button class="df-top" type="button">Back to top <span aria-hidden="true">↑</span></button></p>
</div>
</footer>
<style>
.df{scroll-margin-top:80px;display:block}
.df p{margin:0}
/* ---- sticker band ---- */
.df-band{position:relative;height:70vh;min-height:540px;max-height:860px;background:var(--sun);border-bottom:var(--hair);overflow:hidden;
-webkit-user-select:none;user-select:none}
.df-bhead{position:absolute;inset:0 0 auto 0;z-index:3;display:flex;align-items:center;justify-content:space-between;gap:12px;
padding:14px clamp(16px,1.8vw,26px);pointer-events:none}
.df-bhead>*{pointer-events:auto}
.df-bhint{color:var(--ink);opacity:.7}
.df-bgword{position:absolute;inset:0;z-index:0;display:flex;flex-wrap:wrap;align-content:center;justify-content:center;column-gap:.18em;
font-size:clamp(96px,15.5vw,250px);color:var(--sun-deep);text-align:center;pointer-events:none}
.df-stage{position:absolute;inset:0;z-index:1;isolation:isolate}
.df-st{position:absolute;left:0;top:0;width:clamp(calc(var(--w)*.6px),calc(var(--w)*.06944vw),calc(var(--w)*1.12px));
touch-action:none;cursor:grab;visibility:hidden;will-change:transform;-webkit-tap-highlight-color:transparent}
.df-ready .df-st{visibility:visible}
.df-st.is-grab{cursor:grabbing}
.df-fl{display:block;will-change:transform}
.df-st svg{display:block;width:100%;height:auto;overflow:visible;pointer-events:none}
/* svg atoms (palette only) */
.df-k{fill:var(--ink)} .df-w{fill:var(--bg)} .df-r{fill:var(--tomato)} .df-y{fill:var(--sun-deep)} .df-p{fill:var(--tomato-soft)} .df-n{fill:none}
.df-o{stroke:var(--ink);stroke-width:3;stroke-linejoin:round;stroke-linecap:round}
.df-td{font-family:var(--display);font-weight:900;letter-spacing:.01em}
.df-tp{font-family:var(--pixel);letter-spacing:.04em}
.df-mis.df-r{mix-blend-mode:multiply}
/* html label stickers */
.df-lab{background:var(--bg);border:3px solid var(--ink);padding:6px;border-radius:6px;pointer-events:none}
.df-lab__in{border:2px solid var(--ink);display:flex;flex-direction:column;align-items:center;text-align:center}
.df-lab b{font-family:var(--display);font-weight:900;text-transform:uppercase;line-height:.92;letter-spacing:.01em}
.df-lab--a b{font-size:calc(var(--w)*.16px);padding:10px 8px 6px}
.df-lab__bar{align-self:stretch;background:var(--tomato);color:#fff;font-family:var(--pixel);font-size:10px;text-transform:uppercase;padding:4px 6px;letter-spacing:.06em}
.df-lab__row{display:flex;gap:6px;padding:0 0 10px}
.df-lab__row i{width:10px;height:10px;border:2px solid var(--ink);background:var(--sun-deep)}
.df-lab__row i:nth-child(2n){background:var(--tomato)}
.df-lab--b .df-lab__in{background:var(--sun-deep);padding:12px 10px 10px}
.df-lab--b b{font-size:calc(var(--w)*.13px)}
.df-lab__sub{font-family:var(--pixel);font-size:10px;text-transform:uppercase;margin-top:6px;border-top:2px solid var(--ink);padding-top:5px;letter-spacing:.04em}
/* pixel button (stepped corners) */
.df-pxbtn{display:inline-flex;align-items:center;gap:8px;font-family:var(--pixel);font-size:12px;text-transform:uppercase;letter-spacing:.06em;
padding:10px 16px;background:var(--bg);color:var(--ink);border:2px solid var(--ink);
clip-path:polygon(0 4px,4px 4px,4px 0,calc(100% - 4px) 0,calc(100% - 4px) 4px,100% 4px,100% calc(100% - 4px),calc(100% - 4px) calc(100% - 4px),calc(100% - 4px) 100%,4px 100%,4px calc(100% - 4px),0 calc(100% - 4px));
transition:background .15s steps(2),color .15s steps(2),transform .1s steps(1)}
.df-pxbtn:hover{background:var(--tomato);color:#fff}
.df-pxbtn:active{transform:translateY(2px)}
.df-pxbtn:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.df-shuffle span{display:inline-block;font-family:var(--body);font-size:15px;line-height:1}
/* ---- main grid ---- */
.df-main{display:grid;grid-template-columns:1.6fr .8fr 1.1fr}
.df-c{padding:clamp(16px,1.8vw,26px);min-width:0}
.df-main>.df-c:last-child{border-right:0}
.df-lbl{display:flex;align-items:center;gap:8px;color:var(--mute);margin-bottom:clamp(18px,2.4vw,34px)}
.df-state{display:flex;flex-direction:column}
.df-big{font-size:clamp(46px,6vw,104px);max-width:14ch}
.df-hl{color:var(--tomato)}
.df-note{max-width:42ch;margin-top:auto !important;padding-top:clamp(24px,3vw,44px);color:var(--mute)}
.df-nav{list-style:none;margin:0;padding:0;border-top:var(--hair)}
.df-nav li{border-bottom:var(--hair)}
.df-nl{position:relative;isolation:isolate;display:grid;grid-template-columns:2.4em 1fr auto;align-items:center;gap:8px;
padding:14px 4px;text-decoration:none;font-family:var(--pixel);text-transform:uppercase;font-size:clamp(18px,1.55vw,24px);line-height:1}
.df-nl::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--sun);transform:scaleX(0);transform-origin:right;transition:transform .45s var(--ease)}
.df-nl:hover::before,.df-nl:focus-visible::before{transform:scaleX(1);transform-origin:left}
.df-ni{color:var(--mute);font-size:10px}
.df-nt{white-space:nowrap;overflow:hidden}
.df-na{font-family:var(--body);transition:transform .35s var(--ease)}
.df-nl:hover .df-na,.df-nl:focus-visible .df-na{transform:translateX(4px);color:var(--tomato)}
.df-mail{position:relative;display:inline-block;font-weight:600;font-size:clamp(22px,2.1vw,34px);letter-spacing:-.02em;line-height:1.15;
text-decoration:none;padding-bottom:6px;overflow-wrap:anywhere}
.df-mail::before,.df-mail::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--ink)}
.df-mail::after{height:4px;background:var(--tomato);transform:scaleX(0);transform-origin:right;transition:transform .5s var(--ease)}
.df-mail:hover::after,.df-mail:focus-visible::after{transform:scaleX(1);transform-origin:left}
.df-addr{font-style:normal;margin-top:20px;line-height:1.45}
.df-hours{display:block;margin-top:8px;color:var(--mute)}
.df-soc{list-style:none;margin:22px 0 0;padding:0;display:grid;grid-template-columns:1fr 1fr;border-top:var(--hair);border-left:var(--hair)}
.df-soc li{border-right:var(--hair);border-bottom:var(--hair)}
.df-sl{display:flex;justify-content:space-between;align-items:center;padding:10px 10px;text-decoration:none;font-size:11px}
.df-sl span{font-family:var(--body);font-size:14px;transition:transform .3s var(--ease)}
.df-sl:hover{background:var(--tomato-soft)}
.df-sl:hover span,.df-sl:focus-visible span{transform:translate(2px,-2px);color:var(--tomato)}
.df-form{margin-top:24px}
.df-form-h{color:var(--mute);margin-bottom:10px !important}
.df-field{display:flex;gap:8px;flex-wrap:wrap}
.df-input{flex:1 1 180px;min-width:0;font:inherit;font-size:15px;color:var(--ink);background:var(--bg);border:1px solid var(--ink);border-radius:0;padding:10px 12px}
.df-input::placeholder{color:var(--mute)}
.df-input:focus-visible{outline:2px solid var(--tomato);outline-offset:0}
.df-input[aria-invalid="true"]{border-color:var(--tomato);background:var(--tomato-soft)}
.df-sub{background:var(--sun)}
.df-msg{min-height:1.3em;margin-top:10px !important}
.df-msg.is-err{color:var(--tomato-deep)}
.df-msg.is-ok{color:var(--ink)}
.df-ok{border:var(--hair);background:var(--tomato-soft);padding:14px;display:flex;gap:10px;align-items:flex-start}
.df-ok b{font-family:var(--display);font-weight:900;font-size:26px;text-transform:uppercase;line-height:.9;display:block;margin-bottom:6px}
.df-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}
/* ---- bottom bar ---- */
.df-bar{display:grid;grid-template-columns:1fr 1fr 1.3fr auto}
.df-b{display:flex;align-items:center;gap:8px;padding:12px clamp(16px,1.8vw,26px);min-width:0}
.df-bar>.df-b:last-child{border-right:0}
.df-b--top{justify-content:flex-end}
.df-bl{text-decoration:none;border-bottom:1px solid transparent}
.df-bl:hover{color:var(--tomato);border-bottom-color:currentColor}
.df-top{font-family:var(--pixel);text-transform:uppercase;font-size:11px;display:inline-flex;gap:8px;align-items:center;padding:4px 2px}
.df-top span{font-family:var(--body);font-size:14px;transition:transform .3s var(--ease)}
.df-top:hover{color:var(--tomato)}
.df-top:hover span{transform:translateY(-3px)}
.df-blink{animation:df-blink 1.1s steps(1) infinite}
@keyframes df-blink{50%{opacity:0}}
@media (max-width:1024px){
.df-main{grid-template-columns:1fr 1fr}
.df-state{grid-column:1 / -1;border-right:0}
.df-big{max-width:16ch}
.df-bar{grid-template-columns:1fr 1fr}
.df-bar>.df-b:nth-child(2n){border-right:0}
.df-b--top{justify-content:flex-start}
}
@media (max-width:767px){
.df-band{height:60vh;min-height:460px}
.df-st:not([data-mh]){display:none}
.df-st{width:calc(var(--w)*.6px)}
.df-bhint{display:none}
.df-bgword{font-size:24vw;flex-direction:column;align-items:center}
.df-main{grid-template-columns:1fr}
.df-main>.df-c{border-right:0}
}
@media (max-width:480px){
.df-bar{grid-template-columns:1fr}
.df-bar>.df-b{border-right:0}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.df-blink{animation:none}
.df-nl::before,.df-na,.df-mail::after,.df-sl span,.df-top span{transition:none}
}
</style>
<script>
DD.later(() => {
const root = document.getElementById('contact');
if (!root) return;
const reduce = DD.reduce;
const rnd = (a, b) => a + Math.random() * (b - a);
const shuffleArr = a => { a = a.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
/* ================= STICKER WALL ================= */
const band = root.querySelector('.df-band');
const stage = root.querySelector('.df-stage');
const all = [...stage.querySelectorAll('.df-st')];
const mq = matchMedia('(max-width: 767px)');
const parse = s => s ? s.split(',').map(Number) : null;
all.forEach(s => { s._h = parse(s.dataset.h); s._mh = parse(s.dataset.mh); });
const active = () => all.filter(s => !mq.matches || s._mh);
let W = 0, H = 0, z = 30, slapped = reduce;
const measure = () => { W = stage.clientWidth; H = stage.clientHeight; all.forEach(s => { s._w = s.offsetWidth; s._ht = s.offsetHeight; }); };
const clampXY = (s, x, y) => ({
x: Math.min(Math.max(x, -s._w * .25), W - s._w * .75),
y: Math.min(Math.max(y, -s._ht * .15 + 40), H - s._ht * .8)
});
const fromFrac = s => clampXY(s, s._fx * W - s._w / 2, s._fy * H - s._ht / 2);
const saveFrac = (s, x, y) => { s._fx = (x + s._w / 2) / (W || 1); s._fy = (y + s._ht / 2) / (H || 1); };
const setHome = () => active().forEach(s => { const h = mq.matches ? s._mh : s._h; s._fx = h[0]; s._fy = h[1]; s._r = h[2]; });
const place = () => { measure(); active().forEach(s => { if (s._drag) return; const p = fromFrac(s); gsap.set(s, { x: p.x, y: p.y, rotation: s._r }); }); };
setHome(); place();
active().forEach((s, i) => { s.style.zIndex = i + 1; });
if (!reduce) gsap.set(active(), { opacity: 0, scale: 1.4 });
band.classList.add('df-ready');
// slow individual floats on the inner wrapper (outer transform belongs to drag/slap)
const floats = reduce ? [] : all.map(s => {
const t = gsap.to(s.querySelector('.df-fl'), { y: rnd(4, 8) * (Math.random() < .5 ? -1 : 1), rotation: rnd(-2.5, 2.5),
duration: rnd(2.2, 3.8), ease: 'sine.inOut', yoyo: true, repeat: -1, paused: true });
t.progress(Math.random());
return t;
});
let bandOn = false;
const setFloats = on => floats.forEach(t => on ? t.play() : t.pause());
if (!reduce) ScrollTrigger.create({ trigger: band, start: 'top bottom', end: 'bottom top',
onToggle: self => { bandOn = self.isActive; setFloats(bandOn && slapped); } });
// slap-down entrance, random order
const slap = () => {
if (slapped) return; slapped = true;
shuffleArr(active()).forEach((s, i) => {
if (s._drag) return;
const d = .08 + i * .11;
gsap.set(s, { zIndex: ++z });
gsap.fromTo(s, { scale: 1.4, rotation: s._r + rnd(-28, 28) },
{ scale: 1, rotation: s._r, duration: .7, ease: 'bounce.out', delay: d });
gsap.to(s, { opacity: 1, duration: .1, delay: d });
});
setFloats(bandOn);
};
if (!reduce) ScrollTrigger.create({ trigger: band, start: 'top 65%', once: true, onEnter: slap });
// dragging with inertia toss
all.forEach(s => {
let ox = 0, oy = 0, pid = null, samples = [];
s.addEventListener('pointerdown', e => {
if (e.button > 0) return;
e.preventDefault();
pid = e.pointerId; s.setPointerCapture(pid);
s._drag = true; s.classList.add('is-grab');
gsap.killTweensOf(s);
gsap.set(s, { opacity: 1, zIndex: ++z });
const x = gsap.getProperty(s, 'x'), y = gsap.getProperty(s, 'y');
ox = e.clientX - x; oy = e.clientY - y;
samples = [{ x: e.clientX, y: e.clientY, t: performance.now() }];
gsap.to(s, { scale: 1.08, rotation: gsap.getProperty(s, 'rotation') + rnd(-4, 4), duration: .2, ease: 'power2.out' });
});
s.addEventListener('pointermove', e => {
if (!s._drag || e.pointerId !== pid) return;
const p = clampXY(s, e.clientX - ox, e.clientY - oy);
gsap.set(s, { x: p.x, y: p.y });
const now = performance.now();
samples.push({ x: e.clientX, y: e.clientY, t: now });
while (samples.length > 2 && now - samples[0].t > 90) samples.shift();
});
const end = e => {
if (!s._drag || e.pointerId !== pid) return;
s._drag = false; s.classList.remove('is-grab');
try { s.releasePointerCapture(pid); } catch (_) {}
const x = gsap.getProperty(s, 'x'), y = gsap.getProperty(s, 'y'), r = gsap.getProperty(s, 'rotation');
const now = performance.now(), a = samples[0], b = samples[samples.length - 1];
let vx = 0, vy = 0;
if (a && b && b.t - a.t > 8 && now - b.t < 70) { vx = (b.x - a.x) / (b.t - a.t); vy = (b.y - a.y) / (b.t - a.t); }
if (reduce) { vx = vy = 0; }
const p = clampXY(s, x + vx * 280, y + vy * 280);
saveFrac(s, p.x, p.y);
s._r = r + Math.max(-30, Math.min(30, vx * 14));
gsap.to(s, { x: p.x, y: p.y, rotation: s._r, scale: 1, duration: reduce ? .01 : .9, ease: 'power3.out' });
};
s.addEventListener('pointerup', end);
s.addEventListener('pointercancel', end);
s.addEventListener('dragstart', e => e.preventDefault());
});
// shuffle: re-scatter over a jittered grid so the wall stays covered
root.querySelector('.df-shuffle').addEventListener('click', () => {
const list = active(), cols = mq.matches ? 2 : 6, rows = Math.ceil(list.length / cols);
const slots = [];
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++)
slots.push([(c + .5) / cols + rnd(-.05, .05), .2 + (r + .5) / rows * .72 + rnd(-.05, .05)]);
const picks = shuffleArr(slots);
slapped = true; setFloats(bandOn);
shuffleArr(list).forEach((s, i) => {
s._fx = picks[i][0]; s._fy = picks[i][1]; s._r = rnd(-20, 20);
const p = fromFrac(s);
gsap.killTweensOf(s);
s.style.zIndex = ++z;
if (reduce) gsap.set(s, { x: p.x, y: p.y, rotation: s._r, scale: 1, opacity: 1 });
else gsap.to(s, { x: p.x, y: p.y, rotation: s._r, scale: 1, opacity: 1, duration: .9, ease: 'back.out(1.3)', delay: i * .045 });
});
});
// resize / breakpoint change
let rz, wasMobile = mq.matches;
addEventListener('resize', () => {
clearTimeout(rz);
rz = setTimeout(() => {
if (mq.matches !== wasMobile) {
wasMobile = mq.matches; setHome();
if (slapped) gsap.set(active(), { opacity: 1, scale: 1 });
else gsap.set(active(), { opacity: 0, scale: 1.4 });
}
place();
}, 150);
});
/* ================= STATEMENT: line-by-line reveal ================= */
const big = root.querySelector('.df-big');
const words = DD.split(big, 'words');
if (!reduce) {
gsap.set(words, { yPercent: 110 });
ScrollTrigger.create({ trigger: big, start: 'top 85%', once: true, onEnter: () => {
const lines = [];
let top = null;
words.forEach(w => {
const t = Math.round(w.parentElement.offsetTop);
if (top === null || Math.abs(t - top) > 4) { lines.push([]); top = t; }
lines[lines.length - 1].push(w);
});
lines.forEach((ln, i) => gsap.to(ln, { yPercent: 0, duration: 1, ease: DD.ease.out, delay: i * .13, stagger: .035 }));
} });
}
/* ================= NAV: char scramble ================= */
const glyphs = '#%&*+=<>/?01X$@';
root.querySelectorAll('.df-nl').forEach(a => {
const el = a.querySelector('.df-nt'), txt = el.textContent;
a.setAttribute('aria-label', txt);
let raf = 0;
const run = () => {
if (reduce) return;
cancelAnimationFrame(raf);
const t0 = performance.now(), dur = 420;
const step = now => {
const p = Math.min((now - t0) / dur, 1);
const keep = Math.floor(p * txt.length);
let out = '';
for (let i = 0; i < txt.length; i++) out += (i < keep || txt[i] === ' ') ? txt[i] : glyphs[(Math.random() * glyphs.length) | 0];
el.textContent = out;
if (p < 1) raf = requestAnimationFrame(step); else el.textContent = txt;
};
raf = requestAnimationFrame(step);
};
a.addEventListener('pointerenter', run);
a.addEventListener('focus', run);
});
/* ================= NEWSLETTER (client-side only, nothing is sent) ================= */
const form = root.querySelector('.df-form'), input = root.querySelector('#df-email'), msg = root.querySelector('#df-msg');
const okRe = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
input.addEventListener('input', () => {
if (input.getAttribute('aria-invalid') === 'true' && okRe.test(input.value.trim())) {
input.removeAttribute('aria-invalid'); msg.textContent = ''; msg.className = 'px df-msg';
}
});
form.addEventListener('submit', e => {
e.preventDefault();
const v = input.value.trim();
if (!v || !okRe.test(v)) {
input.setAttribute('aria-invalid', 'true');
msg.className = 'px df-msg is-err';
msg.textContent = v ? 'That address looks smudged — check it and try again.' : 'We need an email to send the ink to.';
input.focus();
if (!reduce) gsap.fromTo(input, { x: -6 }, { x: 0, duration: .5, ease: 'elastic.out(1,.3)' });
return;
}
const field = form.querySelector('.df-field');
const ok = document.createElement('div');
ok.className = 'df-ok';
ok.innerHTML = '<span class="dot" aria-hidden="true" style="margin-top:6px"></span><div><b>Inked in!</b><span class="px">First issue lands on the first Thursday. Kevin says hi.</span></div>';
field.replaceWith(ok);
msg.className = 'px df-msg is-ok';
msg.textContent = 'Subscribed: ' + v;
if (!reduce) gsap.from(ok, { scale: 1.15, rotation: -3, opacity: 0, duration: .6, ease: 'bounce.out' });
DD.refresh();
});
/* ================= BACK TO TOP ================= */
root.querySelector('.df-top').addEventListener('click', () => {
if (DD.lenis) DD.lenis.scrollTo(0, { duration: 1.6 });
else scrollTo({ top: 0, behavior: reduce ? 'auto' : 'smooth' });
});
DD.refresh();
});
</script>
</div>
</body>
</html>
```