grain
the design language every arshnah surface is held to. it exists because the sites kept coming out looking generated, and no amount of picking better colours fixed that.
the one idea
generated design is smooth, centered, and safe. hand made design has grain: asymmetry, a signature, one rough edge that is obviously a choice. every screen needs some.
be a system without becoming twins. a shared spine (the anti template rules, Space Grotesk + JetBrains Mono as the fixed type pair, near monochrome color, real texture) and a per project skin (layout, palette, material, the signature move). the spine is what makes them mine. the skin is what keeps them apart.
materials
each one below is drawn in its own tokens, not the page's. showing a material in someone else's colours is not the material.
signature: blinking cursor, prompt, film grain
signature: the message vanishing into pixels
signature: the peek reveal
signature: grain, stamp, vibe
signature: the theme-following sky
signature: the clock's colon blinks on a real 1s css cycle
signature: camera-flash pulse over the card on mint/reroll
signature: glass-case light sweep on exhibit hover
signature: numbered ports boot in a staggered cascade on load
signature: a verified stamp fades in on card hover
signature: the stage punches down like a press on download
signature: whatever's actually playing spins like it's on a turntable
signature: a perforated dashed seam between write and preview
signature: the needle overshoots before settling, like a real analog meter
signature: a blinking cursor after every huge/big readout
components
a list of things. replaces the card, everywhere.
<div style={{ borderLeft: "2px solid var(--accent)", paddingLeft: 14 }}>
<a href={url}>{name}</a>
<span className="meta">{tag}</span>
<p>{desc}</p>
</div>a label and a value, with dots carrying one to the other.
<div className="lrow">
<span className="k">uptime</span>
<span className="dots" /> {/* border-bottom: 1px dotted */}
<span className="v">8 months</span>
</div>a heading, on anything with a terminal material.
<h1 className="mono lower">
<span className="faint">~/</span>drop<span className="cursor" />
</h1>
/* the cursor is the one flourish. exactly one per project. */
@keyframes blink { 0%,55%{opacity:1} 56%,100%{opacity:0} }
.cursor { width:.55em; height:1.02em; background:var(--accent);
animation: blink 1.15s step-end infinite; }kills the generated look on contact. one fixed overlay, ~0.045.
body::after {
content:""; position:fixed; inset:0; z-index:9999;
pointer-events:none; opacity:.045;
background-image:url("data:image/svg+xml,\
<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'>\
<filter id='g'><feTurbulence type='fractalNoise'\
baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter>\
<rect width='100%' height='100%' filter='url(%23g)'/></svg>");
}feedback for a physical action, on any kiosk or press material. card's mint/reroll, buttons' download.
function punch(el) {
el.classList.remove("punch");
void el.offsetWidth; // replay on repeat clicks
el.classList.add("punch");
}
@keyframes punch {
0% { transform: scale(1); }
40% { transform: scale(.96); }
100% { transform: scale(1); }
}
.punch { animation: punch .32s ease; }a list powering on in sequence before its real state lands. status's numbered ports.
items.forEach((el, i) => {
el.classList.add("boot");
el.style.animationDelay = i * 70 + "ms";
// real data can still land mid-cascade
});
@keyframes boot-flash {
0% { opacity: 1; }
100% { opacity: .4; }
}
.boot { animation: boot-flash .5s ease-out both; }any value that moves (a needle, a fill, a bar) settles past its mark first, like a real analog gauge. slop's meter.
.needle {
transition: left .32s cubic-bezier(.34, 1.56, .64, 1);
}
/* same curve works on width, transform, top —
anything with a target */the tells
the left column is built wrong on purpose. reading the rule never worked as well as seeing the pair.
never
encrypted drops
instead
share a file the server cannot read.
a rounded card with a centred icon in a tinted square came with the template. nobody chose it. the rail carries the same information and somebody had to decide on it.
never
instead
a value parked at a fixed column leaves the rest of the row as nothing. the dots give the whole width a job. and a list where a proportion would do says less than a bar.
never
instead
more when there is something worth putting here.
an empty promise holding a grid cell open is still empty. every space earns its keep, and a gap closes with fewer, bigger things. more small ones just make it look busy.
never
“Leverage our comprehensive platform to seamlessly unlock your creative potential — in today's fast-paced world, it's not just a tool, it's a game-changer.”
instead
“built this because the guestbook kept getting sprayed with SQL injections. none of them worked. here's the receipts.”
buzzwords, hollow intensifiers, and the “it's not X, it's Y” contrast structure are the copy equivalent of the icon in a tinted square. nobody chose them, the template did. say the specific true thing instead. (this is exactly what slop.arshnah.in scores.)
the gut check
- could a generator produce this in one shot? if yes, it is not done.
- what is the one thing a person obviously tuned here?
- is the working thing the hero, or is it buried under a pitch?
- does it look like my other work in spirit, but not in skin?
- would slop.arshnah.in flag the copy? read it out loud. if it sounds like it's pitching instead of telling you a specific true thing, rewrite it.
- is there a gap next to a tall element? that's data already being fetched and thrown away, not a spot for more padding or a bigger heading.
the full rules live in project-notes/grain.md. it is a living file. when something turns out to look generated, the fix goes in there before it goes anywhere else.