everything you can change, in one place. start with a skin, drop into raw CSS when you want pixel control, then teach your desktop new tricks with Lua.
kirari is a bio link that boots like a little desktop. everything you customize lives inside it, so there's no separate settings site.
the fastest restyle. In edit my page you can pick a skin, a page background pattern, drop shadows, color grades and effects, no code needed. Six skins ship built in; try them right here (this page reskins live):
want your own? the skin editor lets you set your palette, fonts and wallpaper, then export it as a code to share or reuse. Whatever skin you're on defines the color variables the CSS section below builds on.
okay, the fun part. Scroll to the very bottom of edit my page and you'll find a plain text box labelled custom CSS. Anything you type in there gets stapled onto your public page, and it loads after the skin does, which is the whole trick: because it comes last, it wins. You can repaint, move, hide, or animate basically anything.
Two things worth burning into your memory before you touch it. One: it only affects your public page, never the dashboard, so refresh your real page in another tab to see changes. Two: if you paste something cursed and the page goes sideways, don't panic. Just empty the box and hit save. There's no way to permanently break it.
You'll see var(--accent) everywhere in the examples. Those are CSS variables: little named boxes your current skin fills in with a value. The skin sets --accent to its pink (or blue, or whatever), and you just say "give me the accent" instead of writing a hex code. Here's what each one is actually painting right now, on the skin you've got picked up top:
--accentyour main color: buttons, active links, highlights--accent-2the secondary, for gradients and accents--on-accenttext that sits ON an accent fill (stays readable)--inkyour normal text color--ink-softthe muted text: captions, timestamps--panelcard background--panel-2a raised / inset surface--linehairline borders and dividers--radiushow round the corners are--font-displayheadings and your name--font-bodyparagraph text (you're reading it)--font-pixelthe tiny pixel labels#ff6fb5. The swatch above is live, so flip the skin dots in the top-right and watch every value change. Lean on the variables and your page follows along with every skin. Hardcode a hex and it'll clash the second you (or a visitor on a different skin) look at it.Your page is built out of a few labelled chunks. Point at these ids and you're targeting a whole section without guessing at class names:
A favourite from sites like rentry: wrap the whole card in an image border (a border-image), so the frame is a little pixel-art ribbon or lace instead of a plain line. Point at #s-card and give it two lines. The first makes room for the frame; the second paints your image into that room. One catch: the card already has its own border baked onto the element, so you add !importantto those two lines to win, and the border-image line must come after the border line (setting borderwipes a border-image, so order matters).
#s-card {
/* line 1: 18px of see-through border = the frame's thickness */
border: 18px solid transparent !important;
/* line 2: your image, sliced 30% in from each edge, tiled 'round' */
border-image: url(YOUR-IMAGE-URL) 30% / 20px / 5px round !important;
}Read border-image as: the image, then how far in to slice each corner (30%), then how thick to draw it (20px), then how far to bleed past the edge (5px), then how to fill the sides. round tiles it neatly; try stretch or repeat for a different feel. Use a PNG with transparent corners and it reads like a real frame.
One thing worth knowing: a border-image is always painted as a square frame, because the spec gives it no way to follow border-radius. A framed card would read as pointy no matter how round you set it, because its own fill keeps painting out to the square corners underneath the frame. So as soon as a frame is on, we draw the card's face on a layer that sits inside the frame, where it rounds properly. Your frame is untouched. The one visible difference: if your frame is see-through (lace, ribbon, anything with holes), the holes now show the page behind the card instead of the card's own background.
"Tweening" is the old animator's word for the frames between two poses. You draw the start and the end, and the computer fills in the middle so it slides smoothly instead of snapping. CSS does exactly this, and it's honestly the thing that makes a page feel aliveinstead of a flat wall of links. There are two ways in.
A transition says "when this property changes, don't jump, glide there over some time." You define a normal look, then a second look for a state like :hover, and the transition line handles the in-between. Hover this box:
#s-links a {
/* property how long the curve */
transition: transform .25s cubic-bezier(.2,.7,.2,1),
box-shadow .25s ease;
}
#s-links a:hover {
transform: translateY(-6px); /* float up 6px */
box-shadow: 0 14px 30px -12px rgba(0,0,0,.4);
}Read the transition line as three parts: what to animate (transform), how long (.25s), and the curve, the personality of the movement. List several, comma-separated, to tween more than one thing at once. A tiny gotcha that trips everyone up: the transitionlives on the resting rule, not the :hover one. Put it on hover only and it'll ease in but snap back.
That last value, the timing function, is the difference between "cheap" and "expensive" feeling. It decides whether the motion starts slow, ends slow, or overshoots. Same distance, same duration, watch how different they feel:
lineareaseease-in-outcubic-bezier(spring)linear is robotic and you almost never want it. ease-in-outis the safe, pretty default. And a cubic-bezier with a number above 1 (like the last one) overshoots and springs back. Great for something playful, too much for everything.
Transitions need a trigger, like a hover or a click. When you want something to move forever with no input (a glow that breathes, an avatar that floats), you write an @keyframesblock: named poses at 0%, 50%,100%, and CSS tweens between them on a loop. These two run on their own:
/* the floating square */
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
#s-top img { /* your avatar */
animation: bob 2.4s ease-in-out infinite;
}
/* the breathing ring */
@keyframes halo {
0% { box-shadow: 0 0 0 0 var(--accent); }
70% { box-shadow: 0 0 0 12px transparent; }
100% { box-shadow: 0 0 0 0 transparent; }
}
#s-top img { animation: halo 1.8s ease-out infinite; }The animation shorthand is: name, duration, curve, then infinite to loop forever (or a number like 3to run it three times). One kindness to your visitors: some people get motion-sick from looping animation, so it's polite to switch heavy stuff off for them:
@media (prefers-reduced-motion: reduce) {
#s-top img { animation: none; }
}Here's a "living link" that uses both. It rests calm, then on hover it glides up, glows in your accent, and its little arrow slides over. Nothing here is hardcoded, so it matches whatever skin you're wearing:
#s-links a {
transition: transform .2s ease, color .2s ease, text-shadow .2s ease;
}
#s-links a:hover {
transform: translateX(4px);
color: var(--accent);
text-shadow: 0 0 10px color-mix(in srgb, var(--accent) 60%, transparent);
}/* space out your name */
#s-top h1 { letter-spacing: 3px; }
/* rounder cards everywhere */
:root { --radius: 24px; }
/* a soft gradient behind the whole page */
main {
background: linear-gradient(160deg,
color-mix(in srgb, var(--accent) 12%, transparent),
transparent 60%);
}
/* hide the guestbook entirely */
#s-notes { display: none; }open the scripts app for a real Lua 5.3 editor. Scripts run in your browser only, so they never execute for visitors. They're hard-sandboxed: no files, no network, no load(), and a runaway loop hits an instruction cap and stops instead of freezing the tab. The only door out of the sandbox is the kirari table.
kirari.every(30, function()
kirari.mood("it is " .. os.date("%H:%M") .. " somewhere (here)")
end)local skins = {"sugar","kuro","noir","cyber","spooky"}
local i = 1
kirari.every(4, function()
kirari.skin(skins[i])
i = i % #skins + 1
end)