Skip to content
Luna UI
Luna UI

Pastel retro OS pieces: windows, dialogs, a joke captcha, buttons, form controls, block progress, a busy hourglass and pixel desktop icons.

9 pieces · 6 colors · MIT license

(3) new guestbook entries

welcome.txt

Welcome to my little corner of the web. Sign the guestbook, take a picture from the folder, and stay a while.

This site is best viewed at 800 × 600 with the volume up.

installing cuteness.exe

72% · almost there

(1) captcha

system error

The cat sat on the keyboard.

Use this kit

  1. Copy the kit CSS once. It holds the shared colours and every piece's styles.
  2. Then copy any piece's HTML from its page below.
Markdown for AI agents

Colors

Bubblegum Window
#F7B5E4
Lilac Glass
#A49BF3
Error Violet
#A445DD
System Navy
#2C2B9F
Dialog Grey
#D9D9E5
Paper White
#FFFFFF

Pieces 9

readme.txt

Completely automated public test to tell computers and humans apart.

Put anything in this window: text, a form, a guestbook or an iframe. It scrolls when it overflows.

Best viewed at 800 × 600.

Webcore window

Luna UI · 3 variants

Kit CSS

Shared tokens, then every piece's CSS in kit order. Each piece's HTML is on its own page.

Kit CSS
/* Webcore colours. Every piece falls back to these values, so set them once to restyle the kit. */
:root {
  --tv-webcore-pink: #f7b5e4;
  --tv-webcore-lilac: #a49bf3;
  --tv-webcore-violet: #a445dd;
  --tv-webcore-navy: #2c2b9f;
  --tv-webcore-grey: #d9d9e5;
  --tv-webcore-paper: #ffffff;
}

/* Webcore window */
/* A pastel retro OS window: dotted frame, title bar controls and a scrolling white page. */
.tv-webcore-window {
  --tv-webcore-window-face: var(--tv-webcore-pink, #f7b5e4);
  box-sizing: border-box;
  width: 100%;
  max-width: 26rem;
  margin: 0 auto;
  padding: 0 6px 6px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background-color: var(--tv-webcore-window-face, #f7b5e4);
  background-image: radial-gradient(color-mix(in srgb, var(--tv-webcore-paper, #ffffff) 75%, transparent) 1px, transparent 1.3px);
  background-size: 4px 4px;
  color: var(--tv-webcore-navy, #2c2b9f);
  font-family: "Lucida Console", "Courier New", ui-monospace, monospace;
  box-shadow: 4px 4px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-window-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 34px;
}

/* A solid chip keeps the title clear of the dotted frame. */
.tv-webcore-window-title {
  min-width: 0;
  margin: 0;
  padding: 1px 6px;
  overflow: hidden;
  background: var(--tv-webcore-window-face, #f7b5e4);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.tv-webcore-window-controls {
  display: flex;
  flex-shrink: 0;
  gap: 4px;
}

.tv-webcore-window-controls > span {
  position: relative;
  box-sizing: border-box;
  width: 22px;
  height: 18px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-window-min::after {
  content: "";
  position: absolute;
  right: 5px;
  bottom: 3px;
  left: 5px;
  height: 2px;
  background: var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-window-max::after {
  content: "";
  position: absolute;
  inset: 3px 5px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  border-top-width: 3px;
}

/* Close: a wide navy key with a white cross. */
.tv-webcore-window-controls > .tv-webcore-window-close {
  width: 34px;
  background: var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-window-close::before,
.tv-webcore-window-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12px;
  height: 2px;
  background: var(--tv-webcore-paper, #ffffff);
  transform: translate(-50%, -50%) rotate(45deg);
}

.tv-webcore-window-close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.tv-webcore-window-body {
  max-height: 11rem;
  overflow: auto;
  padding: 12px 14px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
  font-size: 14px;
  line-height: 1.6;
  letter-spacing: 0.02em;
}

/* Browsers without the chunky scrollbar below still get it in the window's colours. */
@supports not selector(::-webkit-scrollbar) {
  .tv-webcore-window-body {
    scrollbar-color: var(--tv-webcore-window-face, #f7b5e4) var(--tv-webcore-paper, #ffffff);
  }
}

.tv-webcore-window-body p {
  margin: 0 0 0.8em;
}

.tv-webcore-window-body p:last-child {
  margin-bottom: 0;
}

.tv-webcore-window-body:focus-visible {
  outline: 2px dashed var(--tv-webcore-navy, #2c2b9f);
  outline-offset: -5px;
}

/* A chunky scrollbar on a dithered track, where the browser allows it. */
.tv-webcore-window-body::-webkit-scrollbar {
  width: 14px;
}

.tv-webcore-window-body::-webkit-scrollbar-track {
  border-left: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: repeating-conic-gradient(var(--tv-webcore-window-face, #f7b5e4) 0 25%, var(--tv-webcore-paper, #ffffff) 0 50%) 0 0 / 4px 4px;
}

.tv-webcore-window-body::-webkit-scrollbar-thumb {
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-window-face, #f7b5e4);
}

.tv-webcore-window--lilac {
  --tv-webcore-window-face: var(--tv-webcore-lilac, #a49bf3);
}

.tv-webcore-window--grey {
  --tv-webcore-window-face: var(--tv-webcore-grey, #d9d9e5);
}

/* Webcore dialog */
/* A retro OS alert: coloured title bar, grey face, centred message and double-ruled buttons. */
.tv-webcore-dialog {
  --tv-webcore-dialog-bar: var(--tv-webcore-pink, #f7b5e4);
  --tv-webcore-dialog-bar-ink: var(--tv-webcore-navy, #2c2b9f);
  box-sizing: border-box;
  width: 100%;
  max-width: 24rem;
  margin: 0 auto;
  border: 2px solid var(--tv-webcore-lilac, #a49bf3);
  background: var(--tv-webcore-grey, #d9d9e5);
  color: var(--tv-webcore-navy, #2c2b9f);
  font-family: "Lucida Console", "Courier New", ui-monospace, monospace;
  box-shadow: 4px 4px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-dialog-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 30px;
  padding: 3px 4px 3px 10px;
  background: var(--tv-webcore-dialog-bar, #f7b5e4);
  color: var(--tv-webcore-dialog-bar-ink, #2c2b9f);
}

.tv-webcore-dialog-title {
  margin: 0;
  overflow: hidden;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.tv-webcore-dialog-close {
  position: relative;
  flex-shrink: 0;
  box-sizing: border-box;
  width: 22px;
  height: 20px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-dialog-close::before,
.tv-webcore-dialog-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12px;
  height: 2px;
  background: var(--tv-webcore-navy, #2c2b9f);
  transform: translate(-50%, -50%) rotate(45deg);
}

.tv-webcore-dialog-close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.tv-webcore-dialog-body {
  padding: 24px 18px 20px;
  text-align: center;
}

.tv-webcore-dialog-message {
  margin: 0 0 22px;
  font-size: 15px;
  line-height: 1.5;
  letter-spacing: 0.04em;
}

.tv-webcore-dialog-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}

/* An inner rule inside the navy edge, like the old double-lined default button. */
.tv-webcore-dialog-button {
  min-width: 6.5rem;
  min-height: 2.5rem;
  padding: 4px 16px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-pink, #f7b5e4);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: inherit;
  font-size: 14px;
  letter-spacing: 0.06em;
  cursor: pointer;
  box-shadow: inset 0 0 0 3px var(--tv-webcore-pink, #f7b5e4), inset 0 0 0 4px var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-dialog-button:hover {
  background: color-mix(in srgb, var(--tv-webcore-pink, #f7b5e4) 65%, var(--tv-webcore-paper, #ffffff));
  box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--tv-webcore-pink, #f7b5e4) 65%, var(--tv-webcore-paper, #ffffff)), inset 0 0 0 4px var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-dialog-button:active {
  transform: translate(1px, 1px);
}

.tv-webcore-dialog-button:focus-visible {
  outline: 2px dashed var(--tv-webcore-navy, #2c2b9f);
  outline-offset: 3px;
}

.tv-webcore-dialog--violet {
  --tv-webcore-dialog-bar: var(--tv-webcore-violet, #a445dd);
  --tv-webcore-dialog-bar-ink: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-dialog--lilac {
  --tv-webcore-dialog-bar: var(--tv-webcore-lilac, #a49bf3);
  --tv-webcore-dialog-bar-ink: var(--tv-webcore-navy, #2c2b9f);
}

/* Webcore captcha */
/* A joke captcha in retro OS chrome: a big pixel tick, a code field and two keys. */
.tv-webcore-captcha {
  --tv-webcore-captcha-bar: var(--tv-webcore-pink, #f7b5e4);
  box-sizing: border-box;
  width: 100%;
  max-width: 28rem;
  margin: 0 auto;
  border: 2px solid var(--tv-webcore-lilac, #a49bf3);
  background: var(--tv-webcore-grey, #d9d9e5);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: 14px/1.4 "Lucida Console", "Courier New", ui-monospace, monospace;
  box-shadow: 4px 4px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-captcha-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 30px;
  padding: 3px 4px 3px 10px;
  background: var(--tv-webcore-captcha-bar, #f7b5e4);
}

.tv-webcore-captcha-title {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.tv-webcore-captcha-close {
  position: relative;
  flex-shrink: 0;
  box-sizing: border-box;
  width: 22px;
  height: 20px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-captcha-close::before,
.tv-webcore-captcha-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12px;
  height: 2px;
  background: var(--tv-webcore-navy, #2c2b9f);
  transform: translate(-50%, -50%) rotate(45deg);
}

.tv-webcore-captcha-close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.tv-webcore-captcha-body {
  display: grid;
  gap: 22px;
  padding: 26px 18px 18px;
}

.tv-webcore-captcha-check {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
}

/* A dotted box that fills with a big pixel tick. */
.tv-webcore-captcha-box {
  flex-shrink: 0;
  box-sizing: border-box;
  width: 40px;
  height: 34px;
  margin: 0;
  border: 2px dotted var(--tv-webcore-navy, #2c2b9f);
  background: transparent no-repeat center / 36px 30px;
  appearance: none;
  cursor: pointer;
}

.tv-webcore-captcha-box:checked {
  border-color: transparent;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 10' shape-rendering='crispEdges'%3E%3Cpath d='M0 5h2v1h1v1h1v1h1V7h1V6h1V5h1V4h1V3h1V2h1V1h1v2h-1v1h-1v1H9v1H8v1H7v1H6v1H5v1H4V9H3V8H2V7H1V6H0z' fill='%232c2b9f'/%3E%3C/svg%3E");
}

/* The code field, then yes and no; the field gets a little more room for its eight digits. */
.tv-webcore-captcha-actions {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.tv-webcore-captcha-code,
.tv-webcore-captcha-button {
  box-sizing: border-box;
  width: 100%;
  min-width: 0;
  min-height: 2.5rem;
  padding: 4px 6px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  border-radius: 0;
  background: var(--tv-webcore-pink, #f7b5e4);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: inherit;
  letter-spacing: 0.1em;
  text-align: center;
  text-transform: uppercase;
}

.tv-webcore-captcha-code {
  letter-spacing: 0.04em;
  box-shadow: inset 0 0 0 3px var(--tv-webcore-pink, #f7b5e4), inset 0 0 0 4px var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-captcha-button {
  cursor: pointer;
}

.tv-webcore-captcha-button:hover {
  background: color-mix(in srgb, var(--tv-webcore-pink, #f7b5e4) 60%, var(--tv-webcore-paper, #ffffff));
}

.tv-webcore-captcha-button:active {
  transform: translate(1px, 1px);
}

.tv-webcore-captcha-box:focus-visible,
.tv-webcore-captcha-code:focus-visible,
.tv-webcore-captcha-button:focus-visible {
  outline: 2px dashed var(--tv-webcore-navy, #2c2b9f);
  outline-offset: 3px;
}

.tv-webcore-captcha--lilac {
  --tv-webcore-captcha-bar: var(--tv-webcore-lilac, #a49bf3);
}

/* Webcore message bar */
/* A system message strip: inset label, optional speech bubble and a square dismiss key. */
.tv-webcore-message {
  --tv-webcore-message-face: var(--tv-webcore-lilac, #a49bf3);
  --tv-webcore-message-ink: var(--tv-webcore-paper, #ffffff);
  display: flex;
  align-items: center;
  gap: 10px;
  box-sizing: border-box;
  width: 100%;
  max-width: 24rem;
  margin: 0 auto;
  padding: 6px;
  border: 2px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-message-face, #a49bf3);
  color: var(--tv-webcore-message-ink, #ffffff);
  font-family: "Lucida Console", "Courier New", ui-monospace, monospace;
}

.tv-webcore-message-text {
  flex: 1;
  min-width: 0;
  padding: 7px 10px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--tv-webcore-message-ink, #ffffff) 75%, transparent);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-align: center;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.tv-webcore-message-bubble {
  display: none;
  flex-shrink: 0;
  width: 32px;
  height: 20px;
}

.tv-webcore-message-close {
  position: relative;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
  cursor: pointer;
}

.tv-webcore-message-close::before,
.tv-webcore-message-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 2px;
  background: var(--tv-webcore-navy, #2c2b9f);
  transform: translate(-50%, -50%) rotate(45deg);
}

.tv-webcore-message-close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.tv-webcore-message-close:hover {
  background: var(--tv-webcore-pink, #f7b5e4);
}

.tv-webcore-message-close:focus-visible {
  outline: 2px dashed var(--tv-webcore-message-ink, #ffffff);
  outline-offset: 2px;
}

.tv-webcore-message--pink {
  --tv-webcore-message-face: var(--tv-webcore-pink, #f7b5e4);
  --tv-webcore-message-ink: var(--tv-webcore-navy, #2c2b9f);
}

/* Plain: grey face, big navy type, a speech bubble and a lilac key. */
.tv-webcore-message--plain {
  --tv-webcore-message-face: var(--tv-webcore-grey, #d9d9e5);
  --tv-webcore-message-ink: var(--tv-webcore-navy, #2c2b9f);
  border-color: var(--tv-webcore-lilac, #a49bf3);
}

.tv-webcore-message--plain .tv-webcore-message-text {
  padding-inline: 6px;
  border: 0;
  font-size: 20px;
  text-align: left;
}

.tv-webcore-message--plain .tv-webcore-message-count {
  display: none;
}

.tv-webcore-message--plain .tv-webcore-message-bubble {
  display: block;
}

.tv-webcore-message--plain .tv-webcore-message-close {
  background: var(--tv-webcore-lilac, #a49bf3);
}

/* Webcore progress window */
/* A pastel pop-up with a segmented block bar. Set the fill with --tv-webcore-progress-value. */
.tv-webcore-progress {
  --tv-webcore-progress-face: var(--tv-webcore-pink, #f7b5e4);
  --tv-webcore-progress-block: color-mix(in srgb, var(--tv-webcore-lilac, #a49bf3) 70%, var(--tv-webcore-navy, #2c2b9f));
  box-sizing: border-box;
  width: 100%;
  max-width: 24rem;
  margin: 0 auto;
  padding: 4px 14px 12px;
  border: 2px solid var(--tv-webcore-lilac, #a49bf3);
  background: var(--tv-webcore-progress-face, #f7b5e4);
  color: var(--tv-webcore-navy, #2c2b9f);
  font-family: "Lucida Console", "Courier New", ui-monospace, monospace;
  box-shadow: 4px 4px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-progress-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 28px;
}

.tv-webcore-progress-label {
  overflow: hidden;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.tv-webcore-progress-close {
  position: relative;
  flex-shrink: 0;
  box-sizing: border-box;
  width: 18px;
  height: 16px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-progress-close::before,
.tv-webcore-progress-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 10px;
  height: 2px;
  background: var(--tv-webcore-navy, #2c2b9f);
  transform: translate(-50%, -50%) rotate(45deg);
}

.tv-webcore-progress-close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.tv-webcore-progress-track {
  box-sizing: border-box;
  height: 24px;
  margin-top: 6px;
  padding: 3px;
  overflow: hidden;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: color-mix(in srgb, var(--tv-webcore-lilac, #a49bf3) 22%, var(--tv-webcore-paper, #ffffff));
}

/* Blocks with gaps: each 10px block is followed by a 3px gap. */
.tv-webcore-progress-fill {
  display: block;
  width: var(--tv-webcore-progress-value, 60%);
  height: 100%;
  background: repeating-linear-gradient(90deg, var(--tv-webcore-progress-block, #7b72d4) 0 10px, transparent 10px 13px);
}

.tv-webcore-progress-note {
  margin: 8px 0 0;
  font-size: 12px;
  letter-spacing: 0.05em;
  text-align: right;
}

.tv-webcore-progress--lilac {
  --tv-webcore-progress-face: var(--tv-webcore-lilac, #a49bf3);
  --tv-webcore-progress-block: var(--tv-webcore-navy, #2c2b9f);
}

/* Busy: no known end, so a short run of blocks marches across the track. */
.tv-webcore-progress--busy .tv-webcore-progress-fill {
  width: 35%;
  animation: tv-webcore-progress-march 1.8s steps(14) infinite;
}

@keyframes tv-webcore-progress-march {
  from { transform: translateX(-100%); }
  to { transform: translateX(290%); }
}

@media (prefers-reduced-motion: reduce) {
  .tv-webcore-progress--busy .tv-webcore-progress-fill {
    animation: none;
  }
}

/* Webcore hourglass */
/* A pixel hourglass: the top sand drains into the pile, then the glass flips and starts over. */
.tv-webcore-hourglass {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  color: var(--tv-webcore-navy, #2c2b9f);
  font: 14px/1.2 "Lucida Console", "Courier New", ui-monospace, monospace;
  letter-spacing: 0.06em;
}

.tv-webcore-hourglass svg {
  flex-shrink: 0;
  shape-rendering: crispEdges;
  overflow: visible;
}

.tv-webcore-hourglass-glass {
  width: 64px;
  height: 64px;
  animation: tv-webcore-hourglass-flip 3s steps(6) infinite;
}

.tv-webcore-hourglass-paper {
  fill: var(--tv-webcore-paper, #ffffff);
  stroke: var(--tv-webcore-navy, #2c2b9f);
  stroke-width: 1;
}

.tv-webcore-hourglass-cap {
  fill: var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-hourglass-sand {
  fill: var(--tv-webcore-pink, #f7b5e4);
  transform-box: fill-box;
}

.tv-webcore-hourglass-top {
  transform-origin: center bottom;
  animation: tv-webcore-hourglass-drain 3s steps(8) infinite;
}

.tv-webcore-hourglass-pile {
  transform-origin: center bottom;
  animation: tv-webcore-hourglass-fill 3s steps(8) infinite;
}

.tv-webcore-hourglass-stream {
  animation: tv-webcore-hourglass-stream 3s steps(1) infinite;
}

/* Sand runs for the first 80% of the cycle; the flip takes the rest. */
@keyframes tv-webcore-hourglass-flip {
  0%, 80% { transform: rotate(0deg); }
  100% { transform: rotate(180deg); }
}

@keyframes tv-webcore-hourglass-drain {
  0% { transform: scaleY(1); }
  75%, 100% { transform: scaleY(0); }
}

@keyframes tv-webcore-hourglass-fill {
  0% { transform: scaleY(0); }
  75%, 100% { transform: scaleY(1); }
}

@keyframes tv-webcore-hourglass-stream {
  0%, 74% { opacity: 1; }
  75%, 100% { opacity: 0; }
}

.tv-webcore-hourglass-cursor {
  display: none;
  width: 48px;
  height: 48px;
}

/* With cursor: a pixel arrow with a small hourglass tucked under its tip, like a busy desktop. */
.tv-webcore-hourglass--cursor {
  gap: 0;
}

.tv-webcore-hourglass--cursor .tv-webcore-hourglass-cursor {
  display: block;
}

.tv-webcore-hourglass--cursor .tv-webcore-hourglass-glass {
  width: 40px;
  height: 40px;
  margin: 28px 14px 0 -10px;
}

/* Still: a half-run glass with no stream. */
@media (prefers-reduced-motion: reduce) {
  .tv-webcore-hourglass-glass,
  .tv-webcore-hourglass-top,
  .tv-webcore-hourglass-pile,
  .tv-webcore-hourglass-stream {
    animation: none;
  }

  .tv-webcore-hourglass-top,
  .tv-webcore-hourglass-pile {
    transform: scaleY(0.5);
  }
}

/* Webcore button */
/* A retro OS push button. The fill lightens on hover and the button sinks into its shadow when pressed. */
.tv-webcore-button {
  --tv-webcore-button-face: var(--tv-webcore-pink, #f7b5e4);
  --tv-webcore-button-fill: var(--tv-webcore-button-face, #f7b5e4);
  box-sizing: border-box;
  min-width: 6.5rem;
  min-height: 2.5rem;
  padding: 4px 16px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-button-fill, #f7b5e4);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: 14px/1.2 "Lucida Console", "Courier New", ui-monospace, monospace;
  letter-spacing: 0.06em;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 3px 3px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 35%, transparent);
}

.tv-webcore-button:hover {
  --tv-webcore-button-fill: color-mix(in srgb, var(--tv-webcore-button-face, #f7b5e4) 60%, var(--tv-webcore-paper, #ffffff));
}

.tv-webcore-button:active {
  box-shadow: none;
  transform: translate(3px, 3px);
}

.tv-webcore-button:focus-visible {
  outline: 2px dashed var(--tv-webcore-navy, #2c2b9f);
  outline-offset: 4px;
}

.tv-webcore-button:disabled,
.tv-webcore-button[aria-disabled="true"] {
  --tv-webcore-button-fill: var(--tv-webcore-grey, #d9d9e5);
  border-style: dashed;
  color: color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 55%, var(--tv-webcore-grey, #d9d9e5));
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}

/* Default: an inner rule marks the button that Enter presses. */
.tv-webcore-button--default {
  box-shadow: inset 0 0 0 3px var(--tv-webcore-button-fill, #f7b5e4), inset 0 0 0 4px var(--tv-webcore-navy, #2c2b9f), 3px 3px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 35%, transparent);
}

.tv-webcore-button--default:active {
  box-shadow: inset 0 0 0 3px var(--tv-webcore-button-fill, #f7b5e4), inset 0 0 0 4px var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-button--lilac {
  --tv-webcore-button-face: var(--tv-webcore-lilac, #a49bf3);
}

.tv-webcore-button--grey {
  --tv-webcore-button-face: var(--tv-webcore-grey, #d9d9e5);
}

/* Webcore form controls */
/* Retro OS form controls on a dialog face: navy rules, a pink inner line, and pixel-crisp marks. */
.tv-webcore-form {
  --tv-webcore-form-face: var(--tv-webcore-grey, #d9d9e5);
  display: grid;
  gap: 14px;
  box-sizing: border-box;
  width: 100%;
  max-width: 22rem;
  margin: 0 auto;
  padding: 18px;
  border: 2px solid var(--tv-webcore-lilac, #a49bf3);
  background: var(--tv-webcore-form-face, #d9d9e5);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: 14px/1.4 "Lucida Console", "Courier New", ui-monospace, monospace;
}

.tv-webcore-form-field {
  display: grid;
  gap: 5px;
}

.tv-webcore-form-label {
  padding: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.tv-webcore-form-input,
.tv-webcore-form-select {
  box-sizing: border-box;
  width: 100%;
  min-height: 2.5rem;
  padding: 6px 10px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  border-radius: 0;
  background-color: var(--tv-webcore-paper, #ffffff);
  color: inherit;
  font: inherit;
  letter-spacing: 0.04em;
  box-shadow: inset 0 0 0 2px var(--tv-webcore-paper, #ffffff), inset 0 0 0 3px var(--tv-webcore-pink, #f7b5e4);
}

/* The dropdown key: a pink square with a navy arrow, drawn in the background. */
.tv-webcore-form-select {
  padding-right: 42px;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0h10L5 6z' fill='%232c2b9f'/%3E%3C/svg%3E"), linear-gradient(var(--tv-webcore-navy, #2c2b9f), var(--tv-webcore-navy, #2c2b9f)), linear-gradient(var(--tv-webcore-pink, #f7b5e4), var(--tv-webcore-pink, #f7b5e4));
  background-position: right 11px center, right 30px top, right top;
  background-repeat: no-repeat;
  background-size: 10px 6px, 1px 100%, 30px 100%;
  cursor: pointer;
}

.tv-webcore-form-group {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  border: 0;
}

.tv-webcore-form-group .tv-webcore-form-label {
  margin-bottom: 2px;
}

.tv-webcore-form-option {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 2rem;
  cursor: pointer;
}

.tv-webcore-form-box,
.tv-webcore-form-dot {
  flex-shrink: 0;
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  margin: 0;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background-color: var(--tv-webcore-paper, #ffffff);
  background-position: center;
  background-repeat: no-repeat;
  appearance: none;
  cursor: pointer;
}

.tv-webcore-form-box:checked {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' shape-rendering='crispEdges'%3E%3Cpath d='M2 7l3.5 3.5L12 4' fill='none' stroke='%232c2b9f' stroke-width='2.5' stroke-linecap='square'/%3E%3C/svg%3E");
  background-size: 14px 14px;
}

.tv-webcore-form-dot {
  border-radius: 50%;
}

.tv-webcore-form-dot:checked {
  background-image: radial-gradient(var(--tv-webcore-navy, #2c2b9f) 0 4px, transparent 4.5px);
}

.tv-webcore-form-input:focus-visible,
.tv-webcore-form-select:focus-visible,
.tv-webcore-form-box:focus-visible,
.tv-webcore-form-dot:focus-visible {
  outline: 2px dashed var(--tv-webcore-navy, #2c2b9f);
  outline-offset: 3px;
}

.tv-webcore-form--pink {
  --tv-webcore-form-face: var(--tv-webcore-pink, #f7b5e4);
}

/* Webcore desktop icons */
/* Pixel desktop icons on a lavender desk. Each icon is a 16-unit grid drawn at 4x for crisp edges. */
.tv-webcore-icons {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
  gap: 14px 10px;
  box-sizing: border-box;
  width: 100%;
  max-width: 30rem;
  margin: 0 auto;
  padding: 18px 14px;
  list-style: none;
  background-color: color-mix(in srgb, var(--tv-webcore-lilac, #a49bf3) 22%, var(--tv-webcore-paper, #ffffff));
  background-image: radial-gradient(color-mix(in srgb, var(--tv-webcore-lilac, #a49bf3) 45%, transparent) 1px, transparent 1.3px);
  background-size: 8px 8px;
  font-family: "Lucida Console", "Courier New", ui-monospace, monospace;
}

.tv-webcore-icon {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 6px 4px;
  color: var(--tv-webcore-navy, #2c2b9f);
  text-decoration: none;
}

/* A one-pixel pink and mint offset, like a misaligned CRT. */
.tv-webcore-icon svg {
  width: 64px;
  height: 64px;
  shape-rendering: crispEdges;
  filter: drop-shadow(3px 0 0 color-mix(in srgb, var(--tv-webcore-pink, #f7b5e4) 85%, transparent)) drop-shadow(-2px 0 0 #8fe3c8);
}

.tv-webcore-icon-label {
  max-width: 100%;
  padding: 1px 8px;
  overflow: hidden;
  background: var(--tv-webcore-paper, #ffffff);
  font-size: 13px;
  letter-spacing: 0.03em;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tv-webcore-icon:hover .tv-webcore-icon-label,
.tv-webcore-icon:focus-visible .tv-webcore-icon-label {
  background: var(--tv-webcore-navy, #2c2b9f);
  color: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-icon:focus-visible {
  outline: 1px dotted var(--tv-webcore-navy, #2c2b9f);
  outline-offset: 2px;
}

.tv-webcore-icon-line {
  fill: none;
  stroke: var(--tv-webcore-navy, #2c2b9f);
  stroke-width: 1;
}

.tv-webcore-icon-paper { fill: var(--tv-webcore-paper, #ffffff); }
.tv-webcore-icon-pink { fill: var(--tv-webcore-pink, #f7b5e4); }
.tv-webcore-icon-lilac { fill: var(--tv-webcore-lilac, #a49bf3); }
.tv-webcore-icon-violet { fill: var(--tv-webcore-violet, #a445dd); }
.tv-webcore-icon-navy { fill: var(--tv-webcore-navy, #2c2b9f); }

.tv-webcore-icons--clean .tv-webcore-icon svg {
  filter: none;
}
Preview scene HTML and CSS
Preview scene HTML and CSS
<div class="tv-webcore-scene">
  <div class="tv-webcore-scene-column tv-webcore-scene-side">
    <ul class="tv-webcore-icons">
      <li>
        <a class="tv-webcore-icon" href="#">
          <svg viewBox="0 0 16 16" aria-hidden="true">
            <path class="tv-webcore-icon-paper" d="M3.5 1.5h6l3 3v10h-9z" />
            <path class="tv-webcore-icon-pink" d="M9.5 1.5l3 3h-3z" />
            <path class="tv-webcore-icon-lilac" d="M5 12.5l2.5-3.5 1.5 2 1-1 1.5 2.5z" />
            <rect class="tv-webcore-icon-pink" x="5" y="6" width="2" height="2" />
            <path class="tv-webcore-icon-line" d="M3.5 1.5h6l3 3v10h-9zM9.5 1.5v3h3" />
          </svg>
          <span class="tv-webcore-icon-label">image.info</span>
        </a>
      </li>
      <li>
        <a class="tv-webcore-icon" href="#">
          <svg viewBox="0 0 16 16" aria-hidden="true">
            <path class="tv-webcore-icon-lilac" d="M1.5 3.5h5l1.5 1.5h6.5v9h-13z" />
            <path class="tv-webcore-icon-paper" d="M3 6h10v2H3z" />
            <path class="tv-webcore-icon-pink" d="M1.5 7.5h13v6h-13z" />
            <path class="tv-webcore-icon-line" d="M1.5 3.5h5l1.5 1.5h6.5v9h-13zM1.5 7.5h13" />
          </svg>
          <span class="tv-webcore-icon-label">folder</span>
        </a>
      </li>
      <li>
        <a class="tv-webcore-icon" href="#">
          <svg viewBox="0 0 16 16" aria-hidden="true">
            <path class="tv-webcore-icon-paper" d="M1.5 2.5h13v11h-13z" />
            <path class="tv-webcore-icon-violet" d="M1.5 2.5h13v3h-13z" />
            <rect class="tv-webcore-icon-paper" x="11" y="3" width="2" height="2" />
            <path class="tv-webcore-icon-lilac" d="M3 7h6v1H3zM3 9h8v1H3zM3 11h4v1H3z" />
            <path class="tv-webcore-icon-line" d="M1.5 2.5h13v11h-13zM1.5 5.5h13" />
          </svg>
          <span class="tv-webcore-icon-label">prgrm.py</span>
        </a>
      </li>
    </ul>
    <div class="tv-webcore-hourglass tv-webcore-hourglass--cursor" role="status">
      <svg class="tv-webcore-hourglass-cursor" viewBox="0 0 16 16" aria-hidden="true">
        <path class="tv-webcore-hourglass-paper" d="M1.5 1.5v12l3-3 2.5 4.5 2-1-2.5-4.5h4.5z" />
      </svg>
      <svg class="tv-webcore-hourglass-glass" viewBox="0 0 16 16" aria-hidden="true">
        <path class="tv-webcore-hourglass-paper" d="M4.5 3.5h7v2l-2.5 2.5 2.5 2.5v2h-7v-2l2.5-2.5-2.5-2.5z" />
        <path class="tv-webcore-hourglass-sand tv-webcore-hourglass-top" d="M5 4h6v1.5L8 7.5 5 5.5z" />
        <rect class="tv-webcore-hourglass-sand tv-webcore-hourglass-stream" x="7.5" y="7.5" width="1" height="4" />
        <path class="tv-webcore-hourglass-sand tv-webcore-hourglass-pile" d="M5 12h6v-1.5L8 8.5 5 10.5z" />
        <rect class="tv-webcore-hourglass-cap" x="3" y="1" width="10" height="2" />
        <rect class="tv-webcore-hourglass-cap" x="3" y="13" width="10" height="2" />
      </svg>
      <span class="tv-webcore-hourglass-label">loading…</span>
    </div>
  </div>
  <div class="tv-webcore-scene-column">
    <div class="tv-webcore-message" role="status">
      <span class="tv-webcore-message-text"><span class="tv-webcore-message-count">(3)</span> new guestbook entries</span>
      <svg class="tv-webcore-message-bubble" viewBox="0 0 32 20" aria-hidden="true"><path d="M4 1.5h24a2.5 2.5 0 0 1 2.5 2.5v7a2.5 2.5 0 0 1-2.5 2.5H15l-6 5v-5H4A2.5 2.5 0 0 1 1.5 11V4A2.5 2.5 0 0 1 4 1.5z" fill="none" stroke="currentColor" stroke-width="1.5" /></svg>
      <button class="tv-webcore-message-close" type="button" aria-label="Dismiss"></button>
    </div>
    <section class="tv-webcore-window">
      <header class="tv-webcore-window-bar">
        <h3 class="tv-webcore-window-title">welcome.txt</h3>
        <span class="tv-webcore-window-controls" aria-hidden="true">
          <span class="tv-webcore-window-min"></span>
          <span class="tv-webcore-window-max"></span>
          <span class="tv-webcore-window-close"></span>
        </span>
      </header>
      <div class="tv-webcore-window-body" tabindex="0">
        <p>Welcome to my little corner of the web. Sign the guestbook, take a picture from the folder, and stay a while.</p>
        <p>This site is best viewed at 800 × 600 with the volume up.</p>
      </div>
    </section>
    <div class="tv-webcore-progress" style="--tv-webcore-progress-value: 72%">
      <div class="tv-webcore-progress-bar">
        <span class="tv-webcore-progress-label">installing cuteness.exe</span>
        <span class="tv-webcore-progress-close" aria-hidden="true"></span>
      </div>
      <div class="tv-webcore-progress-track" role="progressbar" aria-label="Installing cuteness.exe" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72">
        <span class="tv-webcore-progress-fill"></span>
      </div>
      <p class="tv-webcore-progress-note">72% · almost there</p>
    </div>
  </div>
  <div class="tv-webcore-scene-column">
    <section class="tv-webcore-captcha">
      <header class="tv-webcore-captcha-bar">
        <h3 class="tv-webcore-captcha-title">(1) captcha</h3>
        <span class="tv-webcore-captcha-close" aria-hidden="true"></span>
      </header>
      <div class="tv-webcore-captcha-body">
        <label class="tv-webcore-captcha-check">
          <input class="tv-webcore-captcha-box" type="checkbox" checked>
          <span>I'm not a robot</span>
        </label>
        <div class="tv-webcore-captcha-actions">
          <input class="tv-webcore-captcha-code" type="text" value="00110001" aria-label="Type the code" size="8">
          <button class="tv-webcore-captcha-button" type="button">yes</button>
          <button class="tv-webcore-captcha-button" type="button">no</button>
        </div>
      </div>
    </section>
    <section class="tv-webcore-dialog tv-webcore-dialog--violet">
      <header class="tv-webcore-dialog-bar">
        <h3 class="tv-webcore-dialog-title">system error</h3>
        <span class="tv-webcore-dialog-close" aria-hidden="true"></span>
      </header>
      <div class="tv-webcore-dialog-body">
        <p class="tv-webcore-dialog-message">The cat sat on the keyboard.</p>
        <div class="tv-webcore-dialog-actions">
          <button class="tv-webcore-dialog-button" type="button">ok</button>
          <button class="tv-webcore-dialog-button" type="button">oh, no</button>
        </div>
      </div>
    </section>
    <div class="tv-webcore-scene-buttons">
      <button class="tv-webcore-button tv-webcore-button--default" type="button">enter site</button>
      <button class="tv-webcore-button tv-webcore-button--lilac" type="button">guestbook</button>
    </div>
  </div>
</div>


<style>
/* A pastel desktop: icons and a busy cursor down the side, windows and dialogs in two columns. Stacks when its container is narrow. */
.tv-webcore-scene {
  display: grid;
  grid-template-columns: minmax(0, 9rem) minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 22px;
  align-items: start;
  padding: 24px;
  background-color: color-mix(in srgb, var(--tv-webcore-lilac, #a49bf3) 26%, var(--tv-webcore-paper, #ffffff));
  background-image: radial-gradient(color-mix(in srgb, var(--tv-webcore-lilac, #a49bf3) 50%, transparent) 1px, transparent 1.3px);
  background-size: 8px 8px;
}

.tv-webcore-scene-column {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 22px;
  min-width: 0;
}

.tv-webcore-scene-column > * {
  max-width: none;
  margin: 0;
}

/* The icons sit straight on the desktop, in one column. */
.tv-webcore-scene .tv-webcore-icons {
  grid-template-columns: minmax(0, 1fr);
  padding: 0;
  background: none;
}

.tv-webcore-scene-side .tv-webcore-hourglass {
  justify-self: center;
}

.tv-webcore-scene-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

@container (max-width: 760px) {
  .tv-webcore-scene {
    grid-template-columns: minmax(0, 1fr);
    padding: 16px;
  }

  .tv-webcore-scene .tv-webcore-icons {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
</style>