Design notes

How a page made mostly of nothing got built.

Kado is fiction. The craft isn't. This is the art direction rationale, the five techniques doing the real work, the prompt you can paste to build something like it, and an honest log of what went wrong in each pass.

Type
Shippori Mincho, Inter
Palette
Bone, sumi ink, moss, warm gray
Stack
Hand-written HTML, CSS, JS
Images
None. All of it is code
Concept

Restraint is the hard version.

Kado is a made-up notes app with one rule: it holds a single line in front of you and hides everything else. The name is , kado, a gate. A gate isn't a wall. Thoughts pass through it and the page stays clear. That fiction had to survive contact with a real landing page, which normally means feature grids, badges, and a hero shouting for attention. A page selling quiet can't shout. So it doesn't.

The art direction is , ma: the interval. In Japanese composition the empty part isn't leftover, it's the material. The hero is about 90% bone-colored nothing with one ink-wash orb and one line of type. That only works if the emptiness is measured. Everything sits on an 8px rhythm, the orb is nudged 2.75rem left of the title's center so your eye falls down and to the right instead of sitting still, and the vertical rail pins the right edge so the whitespace has a boundary to be whitespace against. Symmetry would have read as an unfinished template. Asymmetric balance reads as a decision.

Color earns its keep by barely showing up. Bone #F7F5F0 is the field, sumi ink #1C1B18 is the mark, warm gray #DAD5CC draws every hairline, and moss #6B7752 is the only chromatic note on the page. Moss appears maybe six times total: the tier flag, the "kept" tag, a timestamp on hover, the focus ring, the selection highlight. When your accent is that rare, one moss word carries more weight than a button would. Motion follows the same logic. Nothing bounces, nothing slides. Things fade and un-blur over 1.2 to 2 seconds, hairlines draw themselves left to right, and the orb breathes on an 8 second cycle. Scrolling should feel like turning paper, not operating software.

Technique

Five things doing the work.

The whole site is one HTML file. No build step, no framework, no images. These five pieces are where the effort went.

01

Counter-phase ink-wash orb

The money shot. Two stacked radial gradients, blurred, breathing on an 8 second sine. The trick is the halo: it shrinks while the core swells, on the same timeline. In-phase, it reads as a pulsing dot. Counter-phase, it reads as ink bleeding into wet paper.

.orb-core{
  background:
    radial-gradient(34% 34% at 44% 40%,rgba(28,27,24,.8),rgba(28,27,24,0) 100%),
    radial-gradient(50% 50% at 50% 50%,rgba(28,27,24,.38) 30%,rgba(28,27,24,.58) 54%,
                                       rgba(28,27,24,.1) 69%,rgba(28,27,24,0) 76%);
  filter:blur(8px);
  animation:breathe 8s var(--ease-breathe) infinite;
}
/* core swells out ... */
@keyframes breathe{0%,100%{transform:scale(1);opacity:.9}  50%{transform:scale(1.06);opacity:1}}
/* ... while the halo draws in. the wash bleeds instead of pulsing. */
@keyframes breathe-halo{0%,100%{transform:scale(1.02);opacity:.55} 50%{transform:scale(.94);opacity:.95}}
02

Hairlines that draw, with a touch-down dot

Every section break is a 1px rule that draws itself left to right over 2 seconds on a custom ease. Then, 1.5s later, a 4px ink dot fades in at the origin: the point where the brush touched down. It's a detail almost nobody will consciously notice, which is the point. One exception is coded in: the gate beam in the final CTA draws from its middle outward, so it gets no dot.

/* one attribute drives every rule on the page */
.js [data-rule]{
  transform:scaleX(0); transform-origin:0 50%;
  transition:transform 2s var(--ease-ink) var(--d,0s);
}
.js [data-rule].in{transform:scaleX(1)}

/* the brush touch-down, timed to land after the stroke finishes */
.js [data-rule]::after{
  content:""; position:absolute; left:0; top:-1.5px;
  width:4px; height:4px; border-radius:50%; background:var(--ink);
  opacity:0; transition:opacity 1s var(--ease-page) calc(var(--d,0s) + 1.5s);
}
.js [data-rule].in::after{opacity:.7}
.js .gate-beam::after{display:none} /* draws from center: no touch-down point */
03

Washi grain from feTurbulence

Flat bone looks like a CSS background. Paper has tooth. An SVG fractal-noise filter gets crushed to the ink hue by a colour matrix, dropped to 7% alpha, tiled at 180px, and stored as a data-URI in one custom property. A fixed layer multiplies it over the viewport, and the two "paper" surfaces re-apply it locally so they have their own grain instead of borrowing the window's. Zero network cost, about 400 bytes.

/* the alpha channel (0.07) is the whole dial. 0.2 = sandpaper. */
:root{
  --grain:url("data:image/svg+xml,%3Csvg xmlns='...' width='180' height='180'%3E
    %3Cfilter id='g'%3E
      %3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2'
                     seed='7' stitchTiles='stitch'/%3E
      %3CfeColorMatrix type='matrix' values='0 0 0 0 0.11  0 0 0 0 0.106
                                             0 0 0 0 0.094  0 0 0 0.07 0'/%3E
    %3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
}
body::after{ content:""; position:fixed; inset:0; z-index:80; pointer-events:none;
  background-image:var(--grain); mix-blend-mode:multiply; }
.paper::after,.demo-card::after{ /* same token, local tooth */
  content:""; position:absolute; inset:0;
  background-image:var(--grain); mix-blend-mode:multiply; pointer-events:none; }
04

A note that types, rests, and lets go

The live demo. Per-character delays are randomised 34 to 82ms so it doesn't tick like a metronome, and punctuation buys extra time: a comma pauses 300ms, a full stop 420ms. That's what sells it as thinking rather than printing. Then it rests, a moss "kept" tag fades in, and the whole line blurs out. It only runs while it's on screen, so an idle tab costs nothing.

if (ci < s.length){
  var ch = s.charAt(ci); ci++;
  lineEl.textContent += ch;
  var d = ch === " " ? 26 : 34 + Math.random() * 48;  // never a metronome
  if (ch === ",") d += 300;                           // a breath
  if (ch === "." || ch === "?") d += 420;             // a longer one
  setTimeout(step, d);
} else {                                              // type, rest, keep, let go
  cursor.classList.add("is-rest");
  wait(1000).then(function(){ kept.classList.add("show"); return wait(1800); })
            .then(function(){
              textEl.classList.add("is-fading");
              wait(1350).then(next);
            });
}
05

Reveal choreography without dead zones

One IntersectionObserver drives every fade on the page, unobserving on first hit so nothing ever re-hides on scroll-up. Stagger lives in CSS as a --d variable per element, not in JS timers. The bug worth remembering: the observer's -7% bottom margin meant the hero note, pinned to the bottom of the viewport, never qualified as intersecting and sat at opacity:0 forever. Above-the-fold elements shouldn't be waiting on an observer to tell them they're visible.

var io = new IntersectionObserver(function(entries){
  for (var i = 0; i < entries.length; i++){
    if (entries[i].isIntersecting){
      entries[i].target.classList.add("in");
      io.unobserve(entries[i].target);   // reveal once, never re-hide
    }
  }
}, { threshold: 0.12, rootMargin: "0px 0px -7% 0px" });

// the hero is already on screen at load. let its own delays run it,
// or the -7% margin strands anything pinned to the bottom edge.
var heroBits = document.querySelectorAll(".hero [data-reveal],.hero [data-words],.rail");
for (var h = 0; h < heroBits.length; h++) heroBits[h].classList.add("in");
for (var i = 0; i < targets.length; i++)
  if (!targets[i].classList.contains("in")) io.observe(targets[i]);
Asset pipeline

There are no assets.

Not "we optimised the images". There are no images. No assets/ folder shipped, no generation step, no Higgsfield, no stock, no three.js. Every visual on the site is either a glyph or a few lines of CSS, which is the only honest way to build a page about subtraction.

The orb is two radial gradients and a blur (technique 01). The grain is an SVG feTurbulence filter inlined as a data-URI (technique 03). The favicon is the same idea shrunk to 64px: a bone square with one ink circle, written inline into the <link> tag, so it costs a request of zero. The big behind the interlude section is not artwork, it's a text node in Shippori Mincho at 24rem and 5% opacity, breathing on a 14 second cycle. The gate in the final CTA is three 1px divs: two posts and a beam that draws outward from its own center.

The total payload is one 40KB HTML file, plus two Google fonts. That's the entire pipeline.

<!-- the favicon: the orb, at 64px, as a link tag. no file. -->
<link rel="icon" href="data:image/svg+xml,
  %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E
    %3Crect width='64' height='64' fill='%23F7F5F0'/%3E
    %3Ccircle cx='33' cy='30' r='13' fill='%231C1B18'/%3E
  %3C/svg%3E">
Recreate it

Steal the method.

Paste this into Claude. It's structured Role, Task, Context, Format, Constraints, Examples, because vague prompts get vague pages. The constraints are the load-bearing part: naming what's banned does more work than describing what you want.

Copy-paste prompt
ROLE
You're an art director who ships. You write hand-coded HTML/CSS/JS and you treat
whitespace as a material, not as leftover room.

TASK
Build a single-page site (index.html, no build step) for [PRODUCT], a [ONE-LINE
DESCRIPTION]. Beats, in order: nav, hero, proof strip, features with one live
animated demo, product showcase, pricing (3 tiers), final CTA, real footer.

CONTEXT
Mood: Japanese minimalism, ma (negative space), ink on washi, meditative.
Palette: bone #F7F5F0 field, sumi ink #1C1B18 mark, moss #6B7752 accent used
under six times on the whole page, warm gray #DAD5CC for hairlines only.
Type: Shippori Mincho for display, Inter for body, both from Google Fonts.
The hero is ~90% empty. One small ink-wash orb (stacked radial gradients + blur)
breathing on an 8s cycle, and one perfect line of type. That's the whole hero.

FORMAT
One self-contained index.html. Inline CSS and JS. No frameworks, no Tailwind,
no build step, no images. Google Fonts is the only external request allowed.

CONSTRAINTS
- Motion: opacity and blur only. 1.2s to 2s. Custom cubic-beziers defined as
  named tokens in :root. No bounce, no slide, no default ease, no linear.
- Sections are separated by 1px hairlines that draw themselves left to right.
- Paper grain via an inline SVG feTurbulence data-URI, alpha ~0.07, multiplied
  over the page. No image files of any kind, including the favicon.
- A vertical text rail on one edge with a Japanese character plus romaji.
- Scroll reveals via one IntersectionObserver, unobserve on first hit so nothing
  re-hides. Stagger via a CSS custom property per element, not JS timers.
- Every interactive element gets a designed hover AND focus-visible state.
- Wrap all decorative motion in a prefers-reduced-motion fallback.
- Copy: contractions, no em-dashes, no buzzwords, no fake testimonials.
  Blunt and specific. Write real sentences, never lorem ipsum.
- Asymmetric balance. If the composition is centred and symmetric, it reads as
  an unfinished template. Offset something on purpose.

EXAMPLES
Hero line: "One thought at a time." Nothing else. No badge, no sub-CTA row.
Feature copy: "Kado never asks you to come back. No streaks, no dots, no guilt."
Pricing tiers named for materials, not sizes: Paper (free), Ink ($4/mo),
Stone ($96 once). Never Basic / Pro / Enterprise.

THEN
Screenshot it at 1440px and 390px, actually look at the images, list every
weakness you find, and fix them. Do that three times. Pass 1 fixes structure,
pass 2 adds depth, pass 3 is QA (mobile, reduced-motion, console, meta).
Iteration log

Three passes, honestly.

Each pass meant screenshotting at 1440px and 390px, reading the images, and writing down what was actually wrong before touching anything. Two different models did the work, so each entry says which one.

Pass 1
Claude Fable 5
Structure

Eight fixes. The orb was the bad one.

  • The orb read as a smudge, not a wash. First version was a near-solid black blob under a 7px blur: a dark hole in the page. Rebuilt it as two gradients with a soft falloff (opacity ramping .38 to .58 then out to 0 by 76%) so it has a wet edge instead of a hard one.
  • Body copy was muddy. Every ink tint was an rgba() over a multiply-blended grain layer, so text was compositing twice and losing contrast. Swapped all five tints to solid hex. Contrast went back over 4.5:1.
  • The vertical rail was broken. flex-direction:column plus writing-mode:vertical-rl stacked the kanji, line, and romaji on top of each other. It's row in vertical writing mode.
  • Pricing CTAs didn't line up. The three tiers have different list lengths, so the buttons sat at three different heights. Made each tier a flex column with margin-top:auto on the button.
  • Section labels were optically off. The kanji numeral sat baseline-aligned with the top of the h2 instead of its first line. Added padding-top:1.05rem to the meta column.
  • CSS scroll-behavior:smooth fought the screenshot harness and made full-page captures unreliable. Moved smooth scrolling to JS scrollIntoView, which also lets reduced-motion turn it off properly.
  • The demo card floated. It had a fixed min-height that didn't match the column beside it. Switched the grid to align-items:stretch and let it size itself.
  • Grain was too faint to read as paper at 0.055 alpha. Took it to 0.07. Also tightened the final CTA gate: posts 9.5rem down to 7rem, inner width 40rem out to 46rem.
Pass 2
Claude Fable 5
Depth

Eight additions. Subtler, not busier.

  • Word-level hero choreography. The headline went from one fade to five, one per word, 90ms apart, each un-blurring from 10px. The line assembles instead of appearing.
  • Touch-down dots on every hairline. A 4px ink dot fades in at each rule's origin 1.5s after the stroke lands (technique 02).
  • Local grain on paper surfaces. Pulled the noise into a --grain token and re-applied it to the demo card and the page mock, so those surfaces have their own tooth rather than borrowing the fixed viewport layer.
  • Buttons wipe instead of cross-fading. Replaced the background-colour transition with a ::before that scales in from the left on the ink easing. Filled buttons wipe to moss.
  • The "kept" tag. The demo now types, rests a beat, fades a moss "kept" in beside the cursor, then lets the line go. It turned a typing loop into a small story about the product's actual idea.
  • Hover life on the page mock. Kept lines warm from gray to full ink and their timestamp goes moss. The big got a 14s opacity breath. The brand kanji goes moss on hover.
  • Proof strip staggers. Six names, 70ms apart, instead of one block fade.
  • Found a real bug by probing, not looking. The hero note at the bottom-left was invisible in every screenshot. A computed-style probe showed opacity:0, blur(12px): the IntersectionObserver's -7% bottom rootMargin meant an element pinned to the viewport bottom never counted as intersecting. Hero elements now reveal on their own delays at load (technique 05).
Pass 3
Claude Opus 4.8
Final QA

QA, this guide, and the parts Fable didn't reach.

  • Wrote this guide route. It didn't exist. Fable ran out of credits before starting it, so the whole /guide page is Opus work, built on Fable's tokens and components.
  • Restored the hero's asymmetry. Fable's first version offset the orb 3rem left, then dropped it in pass 2, which left the hero dead-centre and template-ish. Put it back at -2.75rem (-1rem on mobile). The brief asked for composed whitespace, and centred isn't composed.
  • Killed 17 em-dashes. They were only CSS comment dividers, never copy, but the rule says none anywhere. Swapped the glyph.
  • Tap targets. Footer links measured about 39px and inline links about 42px at 390px. Padding up to clear 44px on both, with the underline offsets re-tuned so they still sit on the baseline.
  • Suppressed one wrong dot. The new touch-down dot was inheriting onto the final CTA's gate beam, which draws from its centre and therefore has no touch-down point. It also got squashed by the parent's scaleX. Now hidden.
  • Cut two dead rules. A leftover opacity:.999 stacking-context hack that did nothing, and a duplicate .tier-list margin declaration.
  • Verified the rest: zero console errors on both routes, no horizontal overflow at 390px or 1440px, reduced-motion kills the orb, the drop cue, the typing loop and every transition, meta and og tags and the inline favicon present on both pages, then deployed and re-shot the live URL to confirm production matches local.
Attribution

Two models built this.

Claude Fable 5 did the design and the build. The concept, the palette and type choices, the orb, the grain, the hairline system, the typing demo, all of index.html, and iteration passes 1 and 2 are Fable's work.

Fable's usage credits ran out mid-project, before the guide route was started and before pass 3. Claude Opus 4.8 picked it up from there: wrote this guide, ran the pass 3 QA listed above, and deployed the site. The design is Fable's. The finish is Opus's.

Fable 5 Art direction, index.html, passes 1 and 2
Opus 4.8 This guide, pass 3 QA, deploy and live verification
Neither Kado isn't a real product. The company names in the proof strip are invented. Nothing here is a testimonial.