Back to the library
Pilotis panel lift
Variant: Single beat
A content panel loading in: the slab lifts off its supports, the ribbon window sweeps open, and the roof bar settles — one horizontal reveal used as a card or section entrance. All four elements share a single beat here, so the panel reads as one rigid object arriving rather than a set of parts. Travel is short (a few percent of the panel height) because the point is arrival, not distance.
entrancecardrevealarchitecturalloop
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>Pilotis panel lift — single beat</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: 3.2s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
/* Shared sub-clock: the visible entrance inside the loop. */
--_enter: calc(0.26s / var(--speed));
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
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);
padding: clamp(10px, 4vw, 24px);
}
.stage {
width: min(400px, 90vw);
display: grid;
gap: clamp(8px, 2.4vw, 12px);
}
.panel {
position: relative;
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(4px * var(--round));
padding: clamp(12px, 3.6vw, 18px);
display: grid;
gap: clamp(9px, 2.6vw, 13px);
}
/* Roof bar */
.roof {
height: clamp(8px, 2.4vw, 10px);
border-radius: calc(2px * var(--round));
background: var(--olive);
transform-origin: 0% 100%;
animation: roof-set var(--t) var(--ease-out) infinite;
}
/* Slab: the mass that rests on the supports */
.slab {
height: clamp(46px, 15vw, 62px);
border-radius: calc(3px * var(--round));
background:
linear-gradient(90deg, var(--clay) 0 38%, var(--clay-d) 38% 100%);
animation: slab-lift var(--t) var(--ease-out) infinite;
}
/* Ribbon window: a horizontal slot cut through the slab */
.ribbon {
position: absolute;
left: clamp(12px, 3.6vw, 18px);
right: clamp(12px, 3.6vw, 18px);
top: calc(clamp(12px, 3.6vw, 18px) + clamp(8px, 2.4vw, 10px) + clamp(9px, 2.6vw, 13px) + clamp(14px, 4.4vw, 19px));
height: clamp(14px, 4vw, 18px);
background: var(--ivory);
border-radius: calc(2px * var(--round));
transform-origin: 0% 50%;
animation: ribbon-open var(--t) var(--ease-out) infinite;
}
/* Pilotis: the slender supports the mass sits above */
.piers {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: clamp(10px, 3.2vw, 16px);
height: clamp(20px, 6vw, 26px);
}
.pier {
background: var(--slate);
border-radius: calc(2px * var(--round));
transform-origin: 50% 100%;
animation: pier-raise var(--t) var(--ease-out) infinite;
}
.caption {
font-size: clamp(11px, 3vw, 12px);
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--slate);
opacity: 0.5;
text-align: center;
}
/* Held-visible tails: the entrance occupies the first ~8% of the loop,
the rest is dwell so the arrival is legible, not a strobe. */
@keyframes slab-lift {
0% { opacity: 0; transform: translate3d(0, 5%, 0) scale(0.97); }
8% { opacity: 1; transform: none; }
100% { opacity: 1; transform: none; }
}
@keyframes roof-set {
0% { opacity: 0; transform: translate3d(0, 6px, 0) scaleX(0.94); }
8% { opacity: 1; transform: none; }
100% { opacity: 1; transform: none; }
}
@keyframes ribbon-open {
0% { opacity: 0; transform: scaleX(0.04); }
8% { opacity: 1; transform: scaleX(1); }
100% { opacity: 1; transform: scaleX(1); }
}
@keyframes pier-raise {
0% { opacity: 0; transform: scaleY(0.35); }
8% { opacity: 1; transform: scaleY(1); }
100% { opacity: 1; transform: scaleY(1); }
}
@media (max-width: 380px) {
.panel { padding: 12px; }
.caption { letter-spacing: 0.1em; }
}
@media (prefers-reduced-motion: reduce) {
/* Movement drops; a slow shared opacity breath keeps the card alive. */
.slab, .roof, .ribbon, .pier {
animation: soft-breathe calc(var(--cycle) * 2.4 / var(--speed)) ease-in-out infinite;
transform: none;
}
@keyframes soft-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.62; }
}
}
</style>
</head>
<body>
<div class="stage">
<div class="panel">
<div class="roof"></div>
<div class="slab"></div>
<div class="ribbon"></div>
<div class="piers">
<span class="pier"></span>
<span class="pier"></span>
<span class="pier"></span>
<span class="pier"></span>
<span class="pier"></span>
</div>
</div>
<p class="caption">Section ready</p>
</div>
</body>
</html>The variant family
Same idea, one axis moved. Compare them side by side before you commit.