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.
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
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.
72% · almost there
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.
60% · about 2 minutes left
Shared tokens, then every piece's CSS in kit order. Each piece's HTML is on its own page.
/* 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;
}
<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>