Emphasis — circled by hand
Variant: Circled by hand
The loudest of the four and the one to use once on a page: a loop drawn around the phrase in 528ms that closes past where it started, because a hand circling something overlaps its own opening stroke. The overshoot is drawn into the geometry rather than timed, so it costs nothing to keep in step with the family. Fitting a hand mark to a word of unknown aspect is the real problem here, and it takes two things together — preserveAspectRatio="none" to stretch the loop to whatever box the phrase occupies, and vector-effect="non-scaling-stroke" to stop the ink fattening on one axis while it stretches. Nearly symmetric easing, the one place an ease-in half belongs in this library: a hand leaves rest and returns to rest inside a single loop.
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>Emphasis — circled by hand</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. */
--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 — 4.4s, shared across the emphasis family. The mark begins at
14% in every sibling and is fully released by 88%; this one spends
528ms of that window drawing, twice what the underline takes, because
it is travelling four sides instead of one. */
--cycle: 4.4s;
--t: calc(var(--cycle) / var(--speed));
/* Nearly symmetric, and the one place in this family an ease-in half is
correct: a hand leaves rest and returns to rest inside a single
closing loop, and it does not coast to a crawl halfway round. */
--ease-accent: cubic-bezier(0.5, 0, 0.32, 1);
--_read: color-mix(in srgb, var(--slate) 78%, transparent);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(12px, 3vw, 28px);
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
}
.doc {
width: min(360px, 92vw);
padding: clamp(14px, 3.4vw, 22px) clamp(15px, 3.6vw, 24px);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
background: var(--paper);
color: var(--_read);
}
.eyebrow {
font-size: clamp(11.5px, 2.5vw, 13px);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
.copy {
margin-top: clamp(7px, 1.4vw, 11px);
display: grid;
justify-items: start;
}
.line {
white-space: nowrap;
font-size: clamp(13px, 3.1vw, 18px);
line-height: 1.5;
letter-spacing: -0.005em;
}
.term {
position: relative;
color: var(--slate);
font-weight: 500;
}
/* A span rather than the SVG itself takes the insets, because an
absolutely positioned replaced element sizes from its intrinsic ratio
and not from its offsets — the wrapper gives the percentages below
something real to resolve against. The loop deliberately overlaps the
lines above and below: a mark made over finished prose sits on top of
it rather than inside a reserved slot. */
.mark {
position: absolute;
inset: -0.5em -0.58em;
pointer-events: none;
}
.mark svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
}
/* preserveAspectRatio="none" stretches the loop to whatever box the
phrase happens to occupy, and vector-effect holds the ink at a constant
weight while it does — together they are the only way to fit a hand
mark to a word of unknown aspect without the stroke fattening on one
axis. The travel starts at 103 rather than 100 so the dash's round cap
is parked clear of the path: at exactly 100 it still paints, and an
absent stroke shows as a speck on the left edge. */
.loop {
fill: none;
stroke: var(--clay);
stroke-width: 2;
stroke-linecap: round;
stroke-dasharray: 100 200;
animation: circle var(--t) infinite;
}
/* The path closes past where it started — the overshoot is drawn into the
geometry rather than timed, which is why it costs nothing to keep in
step with the rest of the family. It leaves as a fade: ink does not
un-draw, and rewinding the loop would read as the annotation being
taken back rather than the page moving on. The offset rearms at 88.1%
under zero opacity. */
@keyframes circle {
0%,
14% {
stroke-dashoffset: 103;
opacity: 1;
animation-timing-function: var(--ease-accent);
}
26%,
78% {
stroke-dashoffset: 0;
opacity: 1;
animation-timing-function: ease;
}
88% {
stroke-dashoffset: 0;
opacity: 0;
}
88.1%,
100% {
stroke-dashoffset: 103;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
/* The drawing is the travel, so it goes. The loop is closed from the
start and only its opacity moves, on the same stretched clock and the
same beats the other three siblings use. */
.loop {
stroke-dashoffset: 0;
animation-name: circle-flat;
animation-duration: calc(var(--t) * 1.7);
}
@keyframes circle-flat {
0%,
14% {
opacity: 0;
animation-timing-function: ease;
}
24%,
78% {
opacity: 1;
animation-timing-function: ease;
}
90%,
100% {
opacity: 0;
}
}
}
</style>
</head>
<body>
<div class="doc">
<p class="eyebrow">Planner notes</p>
<div class="copy">
<p class="line">Rows come back in index order,</p>
<p class="line">
<span class="term"
>not heap order<span class="mark" aria-hidden="true"
><svg viewBox="0 0 120 44" preserveAspectRatio="none">
<path
class="loop"
pathLength="100"
vector-effect="non-scaling-stroke"
d="M 8 20 C 10 8 34 2 62 2 C 92 2 117 8 116 21 C 115 33 92 42 60 42 C 30 42 5 36 5 22 C 4.6 13 11 6 24 3"
/></svg></span></span
>, so the seek
</p>
<p class="line">still pays for every fetch.</p>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.