Back to the library
Signature edge save
Variant: Ghost
The lightest build: sub-pixel-feeling rules at low ink, a barely-there stain, and the shortest travel in the family, so the save state registers peripherally without pulling the eye off the text. Use it behind or beside content a user is actively reading, where a visible indicator would compete. Opacity does most of the work here rather than stroke, which is why the ink token drops well below the others while the timing chain is byte-for-byte the same.
savestatusambienteditorialloop
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Signature edge save — ghost</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex
(#d97757 / #b85c3e / #788c5d / #b04a3f). */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3));
--rust: oklch(calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3));
/* Clock — every duration in the document runs off --t. */
--cycle: 4.4s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
/* Weight tokens — the one axis this family varies. */
--_rule: 1px;
--_travel: 3%;
--_ink: 0.26;
--_seal: 1px;
--_stain: 0.26;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
color: var(--slate);
}
.card {
width: min(420px, 92vw);
padding: clamp(16px, 5vw, 26px);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(10px * var(--round));
display: grid;
gap: clamp(12px, 3.4vw, 18px);
}
.head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.title {
font-size: clamp(12px, 3.3vw, 13.5px);
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 600;
}
.state {
font-size: clamp(11px, 3vw, 12px);
color: var(--clay-d);
letter-spacing: 0.04em;
opacity: 0.35;
animation: state-ink var(--t) var(--ease-out) infinite;
}
.block {
position: relative;
height: clamp(86px, 26vw, 112px);
overflow: hidden;
border-radius: calc(4px * var(--round));
}
.rules {
position: absolute;
inset: 0;
display: grid;
grid-template-rows: repeat(7, 1fr);
align-content: stretch;
}
.rule {
position: relative;
align-self: center;
height: var(--_rule);
background: var(--slate);
opacity: 0;
transform-origin: left center;
/* Pages settle in from the spine side; ease-out so the stack lands. */
animation: page-settle var(--t) var(--ease-out) infinite;
animation-delay: calc(var(--_i) * 46ms / var(--speed));
}
.rule:nth-child(1) { --_i: 0; width: 88%; }
.rule:nth-child(2) { --_i: 1; width: 94%; }
.rule:nth-child(3) { --_i: 2; width: 79%; }
.rule:nth-child(4) { --_i: 3; width: 96%; }
.rule:nth-child(5) { --_i: 4; width: 84%; }
.rule:nth-child(6) { --_i: 5; width: 91%; }
.rule:nth-child(7) { --_i: 6; width: 72%; }
.stain {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
background: linear-gradient(90deg,
color-mix(in oklab, var(--clay) 0%, transparent) 0%,
var(--clay) 42%,
var(--clay) 58%,
color-mix(in oklab, var(--clay) 0%, transparent) 100%);
opacity: var(--_stain);
transform: translateX(-100%);
mix-blend-mode: multiply;
animation: edge-sweep var(--t) linear infinite;
}
.seal {
position: absolute;
top: 12%;
bottom: 12%;
right: 2%;
width: var(--_seal);
background: var(--rust);
transform-origin: center bottom;
transform: scaleY(0.04);
opacity: 0;
animation: seal-close var(--t) var(--ease-out) infinite;
}
.foot {
display: flex;
align-items: center;
justify-content: space-between;
font-size: clamp(10.5px, 2.9vw, 11.5px);
letter-spacing: 0.06em;
color: color-mix(in oklab, var(--slate) 38%, var(--paper));
}
.tick {
width: clamp(46px, 14vw, 62px);
height: 1px;
background: var(--oat);
border-radius: 999px;
overflow: hidden;
}
.tick i {
display: block;
height: 100%;
width: 100%;
background: var(--olive);
transform-origin: left center;
transform: scaleX(0);
animation: tick-fill var(--t) linear infinite;
}
@keyframes page-settle {
0% { opacity: 0; transform: translateX(calc(var(--_travel) * -1)) scaleX(0.985); }
16%, 74% { opacity: var(--_ink); transform: translateX(0) scaleX(1); }
90%, 100%{ opacity: 0; transform: translateX(0) scaleX(1); }
}
@keyframes edge-sweep {
0%, 34% { transform: translateX(-100%); }
70%, 100% { transform: translateX(100%); }
}
@keyframes seal-close {
0%, 62% { opacity: 0; transform: scaleY(0.04); }
76%, 88% { opacity: 1; transform: scaleY(1); }
100% { opacity: 0; transform: scaleY(1); }
}
@keyframes tick-fill {
0% { transform: scaleX(0); }
72%, 100% { transform: scaleX(1); }
}
@keyframes state-ink {
0%, 58% { opacity: 0.35; }
76%, 92% { opacity: 1; }
100% { opacity: 0.35; }
}
@media (max-width: 380px) {
.card { padding: 14px; gap: 11px; }
.block { height: 82px; }
}
@media (prefers-reduced-motion: reduce) {
.rule {
opacity: var(--_ink);
transform: none;
animation: page-hold calc(var(--t) * 2) var(--ease-in-out) infinite;
animation-delay: calc(var(--_i) * 46ms / var(--speed));
}
.stain {
transform: none;
animation: stain-breathe calc(var(--t) * 2) var(--ease-in-out) infinite;
}
.seal {
transform: scaleY(1);
animation: seal-hold calc(var(--t) * 2) var(--ease-in-out) infinite;
}
.tick i {
transform: scaleX(1);
animation: state-ink calc(var(--t) * 2) var(--ease-in-out) infinite;
}
.state { animation: state-ink calc(var(--t) * 2) var(--ease-in-out) infinite; }
}
@keyframes page-hold {
0%, 100% { opacity: calc(var(--_ink) * 0.55); }
50% { opacity: var(--_ink); }
}
@keyframes stain-breathe {
0%, 100% { opacity: calc(var(--_stain) * 0.4); }
50% { opacity: var(--_stain); }
}
@keyframes seal-hold {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
</style>
</head>
<body>
<div class="card">
<div class="head">
<span class="title">Draft block</span>
<span class="state">committing</span>
</div>
<div class="block">
<div class="rules">
<span class="rule"></span>
<span class="rule"></span>
<span class="rule"></span>
<span class="rule"></span>
<span class="rule"></span>
<span class="rule"></span>
<span class="rule"></span>
</div>
<div class="stain"></div>
<div class="seal"></div>
</div>
<div class="foot">
<span>edge sealed</span>
<span class="tick"><i></i></span>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.