Skip to content
Luna UI
Luna UI

Pastel retro OS pieces for a whole homepage: menu bar, side menu, marquee, windows, dialogs, a joke captcha, buttons, forms, progress and icons.

13 pieces · 6 colors · MIT license

news

✦ the guestbook is open ✦ new art in the gallery ✦ webring links updated ✦ thank you for visiting ♡

loading guestbook…
(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 13

news

✦ the guestbook is open ✦ new art in the gallery ✦ webring links updated ✦ thank you for visiting ♡

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 menu bar */
/* A retro OS menu bar for site links: a brand on the left, menu items that invert on hover, one dropdown. */
.tv-webcore-menubar {
  --tv-webcore-menubar-face: var(--tv-webcore-pink, #f7b5e4);
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 4px 14px;
  box-sizing: border-box;
  width: 100%;
  padding: 3px 6px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-menubar-face, #f7b5e4);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: 14px/1.2 "Lucida Console", "Courier New", ui-monospace, monospace;
  letter-spacing: 0.04em;
  box-shadow: 3px 3px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-menubar-brand {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0 8px;
  border-right: 1px dotted var(--tv-webcore-navy, #2c2b9f);
  color: inherit;
  font-weight: 700;
  text-decoration: none;
  text-transform: uppercase;
}

.tv-webcore-menubar-list {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.tv-webcore-menubar-list > li {
  position: relative;
  display: flex;
}

.tv-webcore-menubar-item {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0 10px;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  list-style: none;
}

.tv-webcore-menubar-item::-webkit-details-marker {
  display: none;
}

/* The current page is underlined; hover and the open menu invert to navy. */
.tv-webcore-menubar-item[aria-current="page"] {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

.tv-webcore-menubar-item:hover,
.tv-webcore-menubar-menu[open] > .tv-webcore-menubar-item {
  background: var(--tv-webcore-navy, #2c2b9f);
  color: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-menubar-menu > .tv-webcore-menubar-item::after {
  content: "";
  margin-left: 8px;
  border: 4px solid transparent;
  border-top-color: currentColor;
  border-bottom: 0;
}

.tv-webcore-menubar-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 2;
  min-width: 11rem;
  margin: 3px 0 0;
  padding: 3px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
  list-style: none;
  box-shadow: 3px 3px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 30%, transparent);
}

.tv-webcore-menubar-option {
  display: flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0 10px;
  color: inherit;
  text-decoration: none;
}

.tv-webcore-menubar-option:hover {
  background: var(--tv-webcore-menubar-face, #f7b5e4);
}

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

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

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

/* Webcore side menu */
/* A framed side menu: a title strip, links with pixel bullets, and the current page in navy. */
.tv-webcore-sidebar {
  --tv-webcore-sidebar-face: var(--tv-webcore-pink, #f7b5e4);
  box-sizing: border-box;
  width: 100%;
  max-width: 14rem;
  margin: 0 auto;
  padding: 0 4px 6px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-paper, #ffffff);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: 14px/1.3 "Lucida Console", "Courier New", ui-monospace, monospace;
  letter-spacing: 0.03em;
  box-shadow: 4px 4px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-sidebar-title {
  margin: 0 -4px 6px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background-color: var(--tv-webcore-sidebar-face, #f7b5e4);
  background-image: radial-gradient(color-mix(in srgb, var(--tv-webcore-paper, #ffffff) 70%, transparent) 1px, transparent 1.3px);
  background-size: 4px 4px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.tv-webcore-sidebar-heading {
  margin: 10px 6px 4px;
  padding-top: 8px;
  border-top: 1px dotted var(--tv-webcore-navy, #2c2b9f);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.tv-webcore-sidebar-list {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Each link gets a two-pixel-square bullet that turns into an arrow on the current page. */
.tv-webcore-sidebar-link {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 2.25rem;
  padding: 0 8px;
  color: inherit;
  text-decoration: none;
}

.tv-webcore-sidebar-link::before {
  content: "";
  flex-shrink: 0;
  width: 6px;
  height: 6px;
  background: var(--tv-webcore-sidebar-face, #f7b5e4);
  box-shadow: 0 0 0 1px var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-sidebar-link:hover {
  background: color-mix(in srgb, var(--tv-webcore-sidebar-face, #f7b5e4) 45%, var(--tv-webcore-paper, #ffffff));
}

.tv-webcore-sidebar-link[aria-current="page"] {
  background: var(--tv-webcore-navy, #2c2b9f);
  color: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-sidebar-link[aria-current="page"]::before {
  width: 0;
  height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 6px solid var(--tv-webcore-paper, #ffffff);
  background: none;
  box-shadow: none;
}

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

.tv-webcore-sidebar-link[aria-current="page"]:focus-visible {
  outline-color: var(--tv-webcore-paper, #ffffff);
}

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

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

/* Webcore marquee box */
/* A marquee ticker without the marquee element: two copies of the text slide left in a loop. */
.tv-webcore-marquee {
  --tv-webcore-marquee-face: var(--tv-webcore-pink, #f7b5e4);
  --tv-webcore-marquee-ink: var(--tv-webcore-navy, #2c2b9f);
  display: flex;
  align-items: stretch;
  box-sizing: border-box;
  width: 100%;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-marquee-face, #f7b5e4);
  color: var(--tv-webcore-marquee-ink, #2c2b9f);
  font: 14px/1.2 "Lucida Console", "Courier New", ui-monospace, monospace;
  letter-spacing: 0.05em;
  box-shadow: 3px 3px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-marquee-label {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  padding: 0 10px;
  border-right: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-navy, #2c2b9f);
  color: var(--tv-webcore-paper, #ffffff);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.tv-webcore-marquee-window {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  border: 2px dotted color-mix(in srgb, var(--tv-webcore-marquee-ink, #2c2b9f) 45%, transparent);
  margin: 3px;
}

.tv-webcore-marquee-track {
  display: flex;
  width: max-content;
  animation-name: tv-webcore-marquee-scroll;
  animation-duration: var(--tv-webcore-marquee-duration, 18s);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.tv-webcore-marquee-text {
  margin: 0;
  padding: 8px 2rem 8px 0;
  white-space: nowrap;
}

/* Hovering or focusing inside the box pauses the text so it can be read. */
.tv-webcore-marquee:hover .tv-webcore-marquee-track,
.tv-webcore-marquee:focus-within .tv-webcore-marquee-track {
  animation-play-state: paused;
}

@keyframes tv-webcore-marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

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

.tv-webcore-marquee--navy {
  --tv-webcore-marquee-face: var(--tv-webcore-navy, #2c2b9f);
  --tv-webcore-marquee-ink: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-marquee--navy .tv-webcore-marquee-label {
  border-right-color: var(--tv-webcore-paper, #ffffff);
  background: var(--tv-webcore-pink, #f7b5e4);
  color: var(--tv-webcore-navy, #2c2b9f);
}

/* Still: the text sits in place and wraps, and the loop copy is hidden. */
@media (prefers-reduced-motion: reduce) {
  .tv-webcore-marquee-track {
    width: auto;
    animation: none;
  }

  .tv-webcore-marquee-text {
    white-space: normal;
  }

  .tv-webcore-marquee-text[aria-hidden="true"] {
    display: none;
  }
}

/* Webcore stamp marquee */
/* A marquee of 88×31 buttons or stamps: two copies of the row slide left in a loop and pause on hover. */
.tv-webcore-stamps {
  --tv-webcore-stamps-face: var(--tv-webcore-pink, #f7b5e4);
  display: flex;
  align-items: stretch;
  box-sizing: border-box;
  width: 100%;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-stamps-face, #f7b5e4);
  color: var(--tv-webcore-navy, #2c2b9f);
  font: 14px/1.2 "Lucida Console", "Courier New", ui-monospace, monospace;
  box-shadow: 3px 3px 0 color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 25%, transparent);
}

.tv-webcore-stamps-label {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  padding: 0 10px;
  border-right: 1px solid var(--tv-webcore-navy, #2c2b9f);
  background: var(--tv-webcore-navy, #2c2b9f);
  color: var(--tv-webcore-paper, #ffffff);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.tv-webcore-stamps-window {
  flex: 1;
  min-width: 0;
  margin: 3px;
  overflow: hidden;
  border: 2px dotted color-mix(in srgb, var(--tv-webcore-navy, #2c2b9f) 45%, transparent);
}

.tv-webcore-stamps-track {
  display: flex;
  width: max-content;
  animation-name: tv-webcore-stamps-scroll;
  animation-duration: var(--tv-webcore-stamps-duration, 24s);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.tv-webcore-stamps-list {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 6px 8px 6px 0;
  list-style: none;
}

.tv-webcore-stamps-list:first-child {
  padding-left: 8px;
}

.tv-webcore-stamps-item {
  display: block;
  line-height: 0;
}

/* Your own buttons and stamps: shown at their pixel size, scaled without blurring. */
.tv-webcore-stamps-item img {
  display: block;
  width: auto;
  height: 31px;
  image-rendering: pixelated;
}

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

.tv-webcore-stamps:hover .tv-webcore-stamps-track,
.tv-webcore-stamps:focus-within .tv-webcore-stamps-track {
  animation-play-state: paused;
}

@keyframes tv-webcore-stamps-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* Placeholder buttons, drawn in CSS so the preview needs no image files. */
.tv-webcore-stamps-badge {
  display: grid;
  place-items: center;
  box-sizing: border-box;
  width: 88px;
  height: 31px;
  padding: 0 4px;
  border: 1px solid var(--tv-webcore-navy, #2c2b9f);
  font-size: 9px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: 0.06em;
  text-align: center;
  text-transform: uppercase;
}

.tv-webcore-stamps-badge--pink {
  background: linear-gradient(90deg, var(--tv-webcore-pink, #f7b5e4), var(--tv-webcore-lilac, #a49bf3));
  color: var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-stamps-badge--navy {
  background: var(--tv-webcore-navy, #2c2b9f);
  color: var(--tv-webcore-pink, #f7b5e4);
}

.tv-webcore-stamps-badge--grey {
  background: var(--tv-webcore-grey, #d9d9e5);
  color: var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-stamps-badge--stripes {
  background: repeating-linear-gradient(45deg, var(--tv-webcore-paper, #ffffff) 0 4px, color-mix(in srgb, var(--tv-webcore-lilac, #a49bf3) 40%, var(--tv-webcore-paper, #ffffff)) 4px 8px);
  color: var(--tv-webcore-navy, #2c2b9f);
}

.tv-webcore-stamps-badge--lilac {
  background: var(--tv-webcore-lilac, #a49bf3);
  color: var(--tv-webcore-paper, #ffffff);
}

.tv-webcore-stamps-badge--violet {
  background: var(--tv-webcore-violet, #a445dd);
  color: var(--tv-webcore-paper, #ffffff);
}

/* Stamps: bigger tiles with a perforated paper edge. */
.tv-webcore-stamps--stamps .tv-webcore-stamps-item img {
  height: 56px;
}

.tv-webcore-stamps--stamps .tv-webcore-stamps-badge {
  width: 99px;
  height: 56px;
  border: 3px dotted var(--tv-webcore-paper, #ffffff);
  box-shadow: 0 0 0 1px var(--tv-webcore-navy, #2c2b9f);
  font-size: 11px;
}

.tv-webcore-stamps--stamps .tv-webcore-stamps-list {
  gap: 10px;
  padding-top: 8px;
  padding-bottom: 8px;
}

/* Still: the first row wraps into a grid of buttons, and the loop copy is hidden. */
@media (prefers-reduced-motion: reduce) {
  .tv-webcore-stamps-track {
    width: auto;
    animation: none;
  }

  .tv-webcore-stamps-list {
    flex-wrap: wrap;
  }

  .tv-webcore-stamps-list[aria-hidden="true"] {
    display: none;
  }
}

/* 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">
  <nav class="tv-webcore-menubar" aria-label="Site">
    <a class="tv-webcore-menubar-brand" href="#">☾ my homepage</a>
    <ul class="tv-webcore-menubar-list">
      <li><a class="tv-webcore-menubar-item" href="#" aria-current="page">home</a></li>
      <li><a class="tv-webcore-menubar-item" href="#">about me</a></li>
      <li>
        <details class="tv-webcore-menubar-menu">
          <summary class="tv-webcore-menubar-item">stuff</summary>
          <ul class="tv-webcore-menubar-dropdown">
            <li><a class="tv-webcore-menubar-option" href="#">art.gallery</a></li>
            <li><a class="tv-webcore-menubar-option" href="#">music.box</a></li>
            <li><a class="tv-webcore-menubar-option" href="#">shrines</a></li>
          </ul>
        </details>
      </li>
      <li><a class="tv-webcore-menubar-item" href="#">guestbook</a></li>
    </ul>
  </nav>
  <div class="tv-webcore-marquee">
    <span class="tv-webcore-marquee-label">news</span>
    <div class="tv-webcore-marquee-window">
      <div class="tv-webcore-marquee-track">
        <p class="tv-webcore-marquee-text">✦ the guestbook is open ✦ new art in the gallery ✦ webring links updated ✦ thank you for visiting ♡</p>
        <p class="tv-webcore-marquee-text" aria-hidden="true">✦ the guestbook is open ✦ new art in the gallery ✦ webring links updated ✦ thank you for visiting ♡</p>
      </div>
    </div>
  </div>
  <div class="tv-webcore-scene-body">
    <div class="tv-webcore-scene-column tv-webcore-scene-side">
      <nav class="tv-webcore-sidebar" aria-label="Site menu">
        <h3 class="tv-webcore-sidebar-title">navigation</h3>
        <ul class="tv-webcore-sidebar-list">
          <li><a class="tv-webcore-sidebar-link" href="#" aria-current="page">home</a></li>
          <li><a class="tv-webcore-sidebar-link" href="#">about me</a></li>
          <li><a class="tv-webcore-sidebar-link" href="#">art gallery</a></li>
          <li><a class="tv-webcore-sidebar-link" href="#">guestbook</a></li>
        </ul>
        <h4 class="tv-webcore-sidebar-heading">elsewhere</h4>
        <ul class="tv-webcore-sidebar-list">
          <li><a class="tv-webcore-sidebar-link" href="#">webring</a></li>
          <li><a class="tv-webcore-sidebar-link" href="#">cool sites</a></li>
        </ul>
      </nav>
      <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 guestbook…</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>
  <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>
    <li>
      <a class="tv-webcore-icon" href="#">
        <svg viewBox="0 0 16 16" aria-hidden="true">
          <path class="tv-webcore-icon-paper" d="M4.5 3.5h9v11h-9z" />
          <path class="tv-webcore-icon-navy" d="M2.5 1.5h9v11h-9z" />
          <rect class="tv-webcore-icon-pink" x="4" y="4" width="6" height="2" />
          <path class="tv-webcore-icon-line" d="M2.5 1.5h9v11h-9zM11.5 3.5h2v11h-9v-2" />
        </svg>
        <span class="tv-webcore-icon-label">text.info</span>
      </a>
    </li>
  </ul>
  <div class="tv-webcore-stamps">
    <span class="tv-webcore-stamps-label">link me</span>
    <div class="tv-webcore-stamps-window">
      <div class="tv-webcore-stamps-track">
        <ul class="tv-webcore-stamps-list">
          <li><a class="tv-webcore-stamps-item" href="#"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--pink">luna ui ♡</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--navy">webcore</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--grey">best viewed 800×600</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--stripes">made with notepad</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--lilac">y2k forever</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--violet">sign my guestbook</span></a></li>
        </ul>
        <ul class="tv-webcore-stamps-list" aria-hidden="true">
          <li><a class="tv-webcore-stamps-item" href="#" tabindex="-1"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--pink">luna ui ♡</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#" tabindex="-1"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--navy">webcore</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#" tabindex="-1"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--grey">best viewed 800×600</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#" tabindex="-1"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--stripes">made with notepad</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#" tabindex="-1"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--lilac">y2k forever</span></a></li>
          <li><a class="tv-webcore-stamps-item" href="#" tabindex="-1"><span class="tv-webcore-stamps-badge tv-webcore-stamps-badge--violet">sign my guestbook</span></a></li>
        </ul>
      </div>
    </div>
  </div>
</div>

<style>
/* A pastel homepage on a desktop: menu bar and news ticker on top, a side menu, windows and dialogs in two columns, icons along the bottom. Stacks when its container is narrow. */
.tv-webcore-scene {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  padding: 22px;
  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-body {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 22px;
  align-items: start;
}

.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;
}

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

/* The icons sit straight on the desktop, in a row along the bottom. */
.tv-webcore-scene .tv-webcore-icons {
  grid-template-columns: repeat(4, minmax(0, 7rem));
  max-width: none;
  margin: 0;
  padding: 0;
  background: none;
}

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

@container (max-width: 760px) {
  .tv-webcore-scene {
    padding: 14px;
  }

  .tv-webcore-scene-body {
    grid-template-columns: minmax(0, 1fr);
  }

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