/* The layout of the blog. public/pixa/pixa.css (loaded first) has the font, the palette and the pixel components. */

* { box-sizing: border-box; }

/* The accent of the home page: a palette color, with a dark shadow and fill like --orange in pixa.css. */
:root { --accent: var(--orange); --accent-shadow: var(--orange-shadow); --accent-fill: var(--orange-fill); }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: "Geist Pixel", ui-monospace, Menlo, monospace;
  font-size: 1rem;
  line-height: 1.7;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip; /* a thrown gallery card flies past the edge of the page */
}

header, main {
  max-width: 52rem;
  margin: 0 auto;
  padding-inline: 1.5rem;
}
header {
  padding-top: 1.5rem;
  display: flex;
  align-items: center;
}
header .home { color: var(--muted); text-decoration: none; }
header .home:hover { color: var(--fg); }
/* The theme button (server.js, app.js): a pixel frame at the right of the header, which moves into its shadow on
   press. It shows the moon on a light page and the sun on a dark page. A page with no palette has one scheme only. */
.theme {
  margin-left: auto; padding: 4px; line-height: 0; color: var(--fg); background: var(--bg); cursor: pointer;
  border: 2px solid var(--muted); box-shadow: 4px 4px 0 var(--pixel-shadow);
}
.theme:hover { border-color: var(--fg); }
.theme:active { transform: translate(4px, 4px); box-shadow: none; }
.theme:focus-visible { outline: 1px solid var(--fg); outline-offset: 4px; }
.theme svg { fill: currentColor; shape-rendering: crispEdges; }
:root:not([data-palette]) .theme, :root[data-theme="light"] .theme .sun, :root[data-theme="dark"] .theme .moon { display: none; }
/* On the home page the header has no home link. It takes no height, and the button moves down into the row of the
   heading: the 2rem padding of main, then half the line of the h1. The heading keeps space for it at the right. */
header:not(:has(.home)) { height: 0; }
header:not(:has(.home)) .theme { position: relative; top: calc(2rem + 1.3 * 1.75rem / 2); }
header:not(:has(.home)) + main > h1:first-child { padding-right: 3.5rem; }

main { padding-top: 2rem; padding-bottom: 6rem; }

h1, h2, h3, h4 { font-weight: 500; line-height: 1.3; }
h1 { font-size: 1.75rem; margin: 0 0 1rem; }
h2 { font-size: 1.375rem; margin: 2.5rem 0 1rem; }
h3 { font-size: 1.125rem; margin: 2rem 0 0.75rem; }
h4 { font-size: 1rem; margin: 1.5rem 0 0.5rem; }
/* "-# text" in markdown (see server.js): small text. Directly under a heading, it moves up close to it. */
.small { font-size: 0.8rem; color: var(--muted); }
:is(h1, h2, h3, h4) + .small { margin-top: -0.75rem; }

a { color: inherit; text-decoration: underline; text-decoration-color: var(--muted); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--fg); }
hr { border: none; border-top: 1px solid var(--rule); margin: 2rem 0; }
img { max-width: 100%; height: auto; }
/* An image alone in its paragraph is a block, so no line gap shows under it. "-# text" directly under it is its
   caption: it moves up to 0.5rem under the image. */
p > img:only-child { display: block; }
p:has(> img:only-child) + .small { margin-top: -0.5rem; }
blockquote {
  margin: 1.5rem 0; padding: 0 1rem; border-left: 1px solid var(--rule);
  color: var(--muted);
}

code { font-family: inherit; font-size: 0.9em; background: var(--code-bg); padding: 0.1em 0.3em; }
/* Geist Pixel is proportional, so code blocks need a monospace font to keep the alignment. */
pre { font-family: ui-monospace, Menlo, monospace; background: var(--code-bg); padding: 1rem; overflow-x: auto; }
pre code { background: none; padding: 0; }
/* A markdown table: a pixel frame with a hard shadow, and 2 px lines between the cells. The borders are separate,
   because in a collapsed table the cell border color wins over the frame color. */
table {
  margin: 1.5rem 0; border-collapse: separate; border-spacing: 0;
  border: 2px solid var(--fg); box-shadow: 4px 4px 0 var(--pixel-shadow);
}
th, td { padding: 0.4rem 0.75rem; border: 0 solid var(--rule); border-width: 0 2px 2px 0; }
:is(th, td):last-child { border-right: 0; }
tr:last-child > td { border-bottom: 0; }

/* The Buttondown form in content/about.md: the label, then the email field and the button in one row. The field has
   the pixel frame of a table. The button is an accent .frame that moves into its shadow on hover and on press. */
.subscribe { display: grid; grid-template-columns: 1fr auto; gap: 0.5rem 0.75rem; max-width: 24rem; margin: 2rem 0 0; }
.subscribe label, .subscribe p { grid-column: 1 / -1; margin: 0; }
.subscribe input { font: inherit; border-radius: 0; padding: 0.3rem 0.75rem; }
.subscribe input[type="email"] {
  min-width: 0; color: var(--fg); background: var(--code-bg);
  border: 2px solid var(--muted); box-shadow: 4px 4px 0 var(--pixel-shadow);
}
.subscribe input[type="email"]:focus { outline: none; border-color: var(--fg); }
.subscribe input[type="submit"] {
  color: var(--accent); background: var(--accent-fill); cursor: pointer;
  border: 2px solid var(--accent); box-shadow: 4px 4px 0 var(--accent-shadow);
}
.subscribe input[type="submit"]:hover { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--accent-shadow); }
.subscribe input[type="submit"]:active { transform: translate(4px, 4px); box-shadow: none; }
.subscribe input[type="submit"]:focus-visible { outline: 1px solid var(--fg); outline-offset: 4px; }

/* home tabs */
.tabs { margin-top: 3rem; display: grid; grid-template-columns: repeat(4, 1fr); }
.tabs input { position: absolute; opacity: 0; }
.tabs label { border-bottom: 1px solid var(--rule); padding: 0.5rem; text-align: center; color: var(--muted); cursor: pointer; }
.tabs label:hover { color: var(--fg); }
.tabs input:checked + label { color: var(--fg); border-color: var(--fg); position: relative; }
.tabs input:focus-visible + label { outline: 1px solid var(--fg); outline-offset: 2px; }
.tabs .posts, .tabs .gallery, .tabs .attention { display: none; grid-column: 1 / -1; margin-top: 1.5rem; }
#tab-writing:checked ~ #writing, #tab-projects:checked ~ #projects, #tab-gallery:checked ~ #gallery,
#tab-attention:checked ~ #attention { display: block; }

/* An absolute box with no offsets stays after the text, so the label text stays centered. */
#tab-writing:checked + label::after {
  content: ""; position: absolute; width: 0.6em; height: 1.2em; margin: 0.3em 0 0 0.1em;
  background: currentColor; animation: blink 1.06s steps(1, end) infinite;
}
/* The icon bottom sits on the text baseline. The negative right margin cancels the icon width, so the word stays
   centered. The negative top margin stops the icon from making the tab taller. */
.tabs label svg { display: none; vertical-align: baseline; margin: -8px calc(-24px - 0.4em) 0 0.4em; fill: currentColor; shape-rendering: crispEdges; }
/* The camera body sits on the baseline, and the photo comes out below it (4 sprite pixels = 6 px). */
.tabs .camera, .tabs .eye { margin-bottom: -6px; }
.tabs .pot { margin-bottom: -3px; }
.tabs input:checked + label svg { display: inline; }
/* A narrow tab has no space at the right of the word, so the word and the icon are centered together. */
@media (max-width: 32rem) {
  .tabs label { padding-inline: 0; font-size: 0.875rem; white-space: nowrap; }
  .tabs label svg { margin-right: 0; margin-left: 0.25rem; }
}
.pot .burner { opacity: 0.3; }
.pot .shine { opacity: 0.45; }
.pot .steam { animation: steam 1s steps(1, end) infinite; }
.pot .flame-a, .pot .flame-b { animation: blink 0.4s steps(1, end) infinite; }
.pot .flame-b { animation-delay: -0.2s; }
.camera .mid { opacity: 0.5; }
.camera .dim { opacity: 0.25; }
.camera .flash { opacity: 0; animation: flash 2.4s steps(1, end) infinite; }
.camera .photo { animation: eject 2.4s steps(1, end) infinite; }
/* The eye looks left, then right, and blinks one time in each 4 s. */
.eye .dim { opacity: 0.5; }
.eye .iris { animation: look 4s steps(1, end) infinite; }
.eye .open { animation: eye-open 4s steps(1, end) infinite; }
.eye .closed { opacity: 0; animation: eye-closed 4s steps(1, end) infinite; }
@keyframes look { 30% { transform: translateX(-2px); } 45% { transform: none; } 55% { transform: translateX(2px); } 70% { transform: none; } }
@keyframes eye-open { 86% { opacity: 0; } 90% { opacity: 1; } }
@keyframes eye-closed { 86% { opacity: 1; } 90% { opacity: 0; } }
@keyframes flash { 12% { opacity: 1; } 20% { opacity: 0; } }
/* The photo starts 4 rows up, behind the clip, and comes out of the slot one row at a time. */
@keyframes eject {
  0%, 88%, 100% { transform: translateY(-4px); }
  28% { transform: translateY(-3px); }
  36% { transform: translateY(-2px); }
  44% { transform: translateY(-1px); }
  52% { transform: none; }
}
/* The wisps repeat every 4 rows, so 4 steps of 1 row make a seamless loop. The mask fades and clips them. */
@keyframes steam { 25% { transform: translateY(-1px); } 50% { transform: translateY(-2px); } 75% { transform: translateY(-3px); } }
@media (prefers-reduced-motion: reduce) {
  .tabs label::after, .tabs svg * { animation: none !important; }
}

/* gallery: one pixel Polaroid at a time. app.js changes the photo with a pixelation (see .pixels). */
/* --h and --w are the most height and width a Polaroid photo can have. The deck has the height of the card on show,
   so no gap shows. */
.deck { --h: min(46rem, 80vh); --w: min(34rem, calc(100vw - 5.5rem)); display: grid; justify-items: center; align-items: start; user-select: none; -webkit-user-select: none; }
.deck:focus-visible { outline: 1px solid var(--fg); outline-offset: 4px; }
.card {
  display: none; position: relative; grid-area: 1 / 1; margin: 0; padding: 0.5rem 0.5rem 0; color: #6b6556;
  /* A 9x9 pixel frame: a stepped outline, two light tones at the top and left,
     two shade tones at the bottom and right. 4 frame pixels scale to the 8 px border, so 1 frame pixel is 2 px. */
  border: 8px solid transparent;
  border-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9' shape-rendering='crispEdges'%3E%3Cpath fill='%23221f1a' d='M2 0h1v1H2zM3 0h1v1H3zM4 0h1v1H4zM5 0h1v1H5zM6 0h1v1H6zM1 1h1v1H1zM7 1h1v1H7zM0 2h1v1H0zM8 2h1v1H8zM0 3h1v1H0zM8 3h1v1H8zM0 4h1v1H0zM8 4h1v1H8zM0 5h1v1H0zM8 5h1v1H8zM0 6h1v1H0zM8 6h1v1H8zM1 7h1v1H1zM7 7h1v1H7zM2 8h1v1H2zM3 8h1v1H3zM4 8h1v1H4zM5 8h1v1H5zM6 8h1v1H6z'/%3E%3Cpath fill='%23ffffff' d='M2 1h1v1H2zM3 1h1v1H3zM4 1h1v1H4zM5 1h1v1H5zM6 1h1v1H6zM1 2h1v1H1zM1 3h1v1H1zM1 4h1v1H1zM1 5h1v1H1zM1 6h1v1H1z'/%3E%3Cpath fill='%23f8f6ef' d='M2 2h1v1H2zM3 2h1v1H3zM4 2h1v1H4zM5 2h1v1H5zM2 3h1v1H2zM2 4h1v1H2zM2 5h1v1H2z'/%3E%3Cpath fill='%23efece2' d='M6 2h1v1H6zM3 3h1v1H3zM4 3h1v1H4zM5 3h1v1H5zM3 4h1v1H3zM3 5h1v1H3zM2 6h1v1H2z'/%3E%3Cpath fill='%23a39b84' d='M7 2h1v1H7zM7 3h1v1H7zM7 4h1v1H7zM7 5h1v1H7zM7 6h1v1H7zM2 7h1v1H2zM3 7h1v1H3zM4 7h1v1H4zM5 7h1v1H5zM6 7h1v1H6z'/%3E%3Cpath fill='%23cfc9b6' d='M6 3h1v1H6zM6 4h1v1H6zM6 5h1v1H6zM3 6h1v1H3zM4 6h1v1H4zM5 6h1v1H5zM6 6h1v1H6z'/%3E%3Cpath fill='%23e6e2d5' d='M5 4h1v1H5zM4 5h1v1H4zM5 5h1v1H5z'/%3E%3C/svg%3E") 4 stretch;
  /* The paper: a checkerboard dither of 2 px squares in two near tones. */
  background: repeating-conic-gradient(#efece2 0 25%, #e6e2d5 0 50%) 0 0 / 4px 4px padding-box;
  filter: drop-shadow(4px 4px 0 var(--pixel-shadow));
  cursor: pointer;
}
/* A strip of tape with stepped, torn ends. */
.card::before {
  content: ""; position: absolute; top: -14px; left: 50%; width: 56px; height: 20px; margin-left: -28px;
  background: linear-gradient(rgb(255 255 255 / 0.4) 0 2px, transparent 2px), rgb(222 208 156 / 0.8);
  clip-path: polygon(0 0, calc(100% - 2px) 0, calc(100% - 2px) 4px, 100% 4px, 100% 8px, calc(100% - 2px) 8px, calc(100% - 2px) 12px,
    100% 12px, 100% 16px, calc(100% - 2px) 16px, calc(100% - 2px) 20px, 0 20px, 0 16px, 2px 16px, 2px 12px, 0 12px, 0 8px,
    2px 8px, 2px 4px, 0 4px);
}
.card.on { display: block; }
.card img {
  display: block; aspect-ratio: var(--ar); height: auto; object-fit: cover;
  width: min(var(--w), calc((var(--h) - 6rem) * var(--ar)));
  /* The photo window: a dark edge, a shade band at the top and left, and a light band at the bottom and right. */
  box-shadow: 0 0 0 2px #221f1a, -4px -4px 0 #a39b84, 4px 4px 0 #fff;
  pointer-events: none; -webkit-user-drag: none;
}
/* app.js draws the photo on this canvas in big pixels during a change, then removes it. */
.card .pixels { position: absolute; image-rendering: pixelated; pointer-events: none; }
/* The last card, after all the photos: an unexposed Polaroid. */
.card.end p { display: grid; place-items: center; width: min(var(--w), calc(var(--h) - 6rem)); aspect-ratio: 1; margin: 0; padding: 1rem; background: #111; color: #f1efe7; text-align: center; }
/* One line. The zero width keeps a long text from making the card wider. */
.card figcaption {
  width: 0; min-width: 100%; min-height: 1lh; padding: 0.5rem 0 0.7rem;
  font-size: 1rem; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* The font has one weight. A copy 1 px to the right makes a sharp pixel bold. */
  color: #3a352b; text-shadow: 1px 0 0 currentColor;
}

/* attention panel (content/attention.md): "label: value" lines. On a phone the tab has the title, so the
   panel does not show its own heading. */
.attention h2 { display: none; font-size: 1rem; margin: 0 0 0.85rem; }
.attention ul { list-style: none; padding: 0; margin: 0.5rem 0 0; }
.attention li { margin: 0.2rem 0; }
.attention strong { font-weight: inherit; color: var(--muted); }

/* Wide screen (home page only). Left half: the summary (content/about.md) and the writing and projects tabs,
   which scroll with the page. Right half, fixed to the window: the gallery, and the attention panel in the bottom
   20 %. The gallery and attention have no tab here. app.js changes the photo every 10 s. */
@media (min-width: 64rem) {
  body:has(#gallery) :is(header, main) { width: min(40rem, 50vw); margin-left: max(0px, (50vw - 40rem) / 2); }
  .tabs { grid-template-columns: repeat(2, 1fr); }
  #tab-gallery, label[for="tab-gallery"], #tab-attention, label[for="tab-attention"] { display: none; }
  #gallery, #tab-gallery:checked ~ #gallery {
    display: flex; flex-direction: column; justify-content: center; position: fixed; inset: 0 0 20vh 50vw;
    margin: 0; border-left: 1px solid var(--rule);
  }
  #gallery .deck { --h: calc(80vh - 3rem); --w: calc(50vw - 6rem); }
  #attention, #tab-attention:checked ~ #attention {
    display: block; position: fixed; inset: auto 0 0 50vw; height: 20vh; margin: 0; overflow-y: auto;
    padding: 0.75rem max(1.5rem, (50vw - 40rem) / 2); border-top: 1px solid var(--rule); border-left: 1px solid var(--rule);
  }
  .attention h2 { display: block; }
}

/* post and project lists */
.posts { list-style: none; padding: 0; margin: 0; }
.posts li { margin: 0 0 1.25rem; }
.posts time { display: block; color: var(--muted); font-size: 0.85rem; }
.posts p { margin: 0.15rem 0 0; color: var(--muted); font-size: 0.9rem; }
/* Article and project titles: the accent color, like the nickname link on the home page. */
:is(#writing, #projects) li > :first-child { color: var(--accent); text-decoration-color: var(--accent-shadow); }
:is(#writing, #projects) li > a:first-child:hover { text-decoration-color: var(--accent); }

/* nickname article */
a.nick { color: var(--accent); text-decoration-color: var(--accent-shadow); } /* the home page heading: a link to the article */
a.nick:hover { text-decoration-color: var(--accent); }
.nickname { font-size: 2rem; margin: 0.25rem 0; }
.hash { font-size: 0.8rem; color: var(--muted); margin: 0 0 1.5rem; overflow-wrap: anywhere; }
.sources { font-size: 0.8rem; color: var(--muted); margin: 0; }
/* The state of each fingerprint source (see public/nickname.js and content/fingerprints-are-the-new-cookies/client.js). A line style also
   shows it, not only the color. */
.sources .clean { --state: var(--c-green); }
.sources .tampered { --state: var(--c-yellow); text-decoration: underline wavy; text-underline-offset: 3px; }
.sources .unavailable { --state: var(--muted); text-decoration: line-through; }
.sources [data-tip] { position: relative; color: var(--state); cursor: help; outline: none; }
.frame .hash { margin-bottom: 0.25rem; }
.note { background: var(--code-bg); border: 1px solid var(--rule); padding: 0.75rem 1rem; font-size: 0.95rem; }
/* A tooltip on a word: <span class="tip" tabindex="0" data-tip="text">word</span>. It shows on hover, and on tap or
   keyboard focus (tabindex). The parent of the word is its position box, so it stays inside the text column. */
.tip { text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
:has(> .tip) { position: relative; }
.tip:is(:hover, :focus)::after {
  content: attr(data-tip); position: absolute; left: 0; bottom: calc(100% + 0.5rem); z-index: 20;
  width: max-content; max-width: min(24rem, 100%); padding: 0.5rem 0.75rem;
  font-size: 0.8rem; line-height: 1.5; color: var(--fg); background: var(--bg);
  border: 2px solid var(--fg); box-shadow: 4px 4px 0 var(--pixel-shadow);
}
.tip:focus:not(:focus-visible) { outline: none; }

/* highlight.js token colors (minimal) */
.hljs-comment, .hljs-quote { color: var(--muted); font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-built_in, .hljs-literal { color: var(--c-pink); }
.hljs-string, .hljs-attr, .hljs-template-string { color: var(--c-green); }
.hljs-number, .hljs-symbol { color: var(--c-yellow); }
.hljs-title, .hljs-function .hljs-title, .hljs-section { color: inherit; font-weight: 700; }
/* TETRA timeslots (content/sdr-tetra-analyzer/server.js). --T is the time of one slot: 1 s, or 14.25 ms in real time.
   The tape of slots moves one frame to the left in 4 slot times, in 2 px jumps, and loops. The radio of slot k is on
   air in the k-th slot time of each frame, so the slot under the "now" line and the radio on air always match. A slot
   is 48 px (46 px and a 2 px guard gap), a frame is 4 slots. */
.slots { --T: 1s; --slot: 48px; --frame: calc(4 * var(--slot)); margin: 2rem 0; }
.slots:has(.real:checked) { --T: 14.25ms; }
/* The speed: two radio buttons in pixel frames. The checked one is filled. */
.slots-speed { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; font-size: 0.8rem; }
.slots-speed label { padding: 0.1rem 0.6rem; color: var(--muted); border: 2px solid var(--rule); cursor: pointer; }
.slots-speed label:hover { color: var(--fg); }
.slots-speed label:has(:checked) { color: var(--bg); background: var(--fg); border-color: var(--fg); }
.slots-speed label:has(:focus-visible) { outline: 1px solid var(--fg); outline-offset: 2px; }
.slots .fast, .slots:has(.real:checked) .slow { display: none; }
.slots:has(.real:checked) .fast { display: inline; }
.slots-air { position: relative; overflow: hidden; padding-top: 1.4rem; }
.slots-tape { display: flex; width: max-content; animation: slots-run calc(4 * var(--T)) steps(96) infinite; }
@keyframes slots-run { to { transform: translateX(calc(-1 * var(--frame))); } }
.slots-tape > span { display: flex; flex-wrap: wrap; width: var(--frame); }
.slots-tape i {
  position: relative; box-sizing: border-box; width: calc(var(--slot) - 2px); height: 48px; margin-right: 2px;
  padding: 2px 4px; font-size: 0.7rem; line-height: 1; font-style: normal; color: var(--c);
  background: color-mix(in srgb, var(--c) 14%, var(--bg)); border-top: 2px solid var(--c);
}
/* The burst: columns of pixels, like the bits of the slot. */
.slots-tape i::after {
  content: ""; position: absolute; left: 4px; right: 4px; bottom: 8px; height: 16px;
  background: repeating-linear-gradient(90deg, var(--c) 0 2px, transparent 2px 4px, var(--c) 4px 8px, transparent 8px 10px);
}
/* A bracket under each frame, and its label. */
.slots-tape > span::before {
  content: ""; order: 1; box-sizing: border-box; width: calc(var(--frame) - 2px); height: 6px; margin-top: 4px;
  border: 2px solid var(--muted); border-top: 0;
}
.slots-tape > span::after {
  content: "frame ≈ 57 ms"; order: 2; width: calc(var(--frame) - 2px); padding-top: 2px;
  font-size: 0.7rem; text-align: center; color: var(--muted);
}
/* The "now" line sits one frame from the left. The slots to its left are past, so they are dim. */
.slots-air::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: var(--frame); z-index: 1;
  background: color-mix(in srgb, var(--bg) 65%, transparent);
}
.slots-now {
  position: absolute; left: var(--frame); top: 0; z-index: 2; height: calc(1.4rem + 48px);
  font-size: 0.7rem; line-height: 1rem; color: var(--fg); border-left: 2px solid var(--fg); padding-left: 4px;
}
/* A radio for each slot, with its user: in its slot color while on air, grey while silent. While on air, waves leave
   the antenna and a speech bubble shows the words of the user. */
.slots-radios { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-top: 1rem; }
.slots-radios > div {
  --on: calc(4 * var(--T)) steps(1, end) calc((var(--k) - 5) * var(--T)) infinite;
  position: relative; display: grid; justify-items: center; gap: 0.25rem; padding: 0.5rem 0.25rem; font-size: 0.8rem;
  color: color-mix(in srgb, var(--c) calc(var(--air) * 100%), var(--muted)); box-shadow: 4px 4px 0 var(--pixel-shadow);
  border: 2px solid color-mix(in srgb, var(--c) calc(var(--air) * 100%), var(--rule)); animation: slots-air var(--on);
}
.slots-radios em { font-style: normal; font-size: 0.7rem; }
.slots-radios .waves { fill: var(--c); opacity: 0; animation: slots-on var(--on); }
.slots-radios .say {
  position: absolute; top: -0.9rem; left: 55%; z-index: 1; padding: 0 0.3rem; font-size: 0.7rem; white-space: nowrap;
  color: var(--c); background: var(--bg); border: 2px solid var(--c); opacity: 0; animation: slots-on var(--on);
}
/* The tail of the bubble: a stepped pixel corner. */
.slots-radios .say::after {
  content: ""; position: absolute; left: 2px; top: 100%; width: 6px; height: 4px; background: var(--c);
  clip-path: polygon(0 0, 100% 0, 100% 2px, 4px 2px, 4px 4px, 0 4px);
}
.slots-radios .px { margin: 0; }
.slots-radios :is(.on, .off) { grid-area: 5 / 1; padding: 0 0.3rem; font-size: 0.7rem; }
.slots-radios .on { color: var(--bg); background: var(--c); opacity: 0; animation: slots-on var(--on); }
.slots-radios .off { animation: slots-off var(--on); }
/* --air is 1 while on air, else 0, so the mix above is all slot color or all grey. The keyframes animate a number and
   not a color: Chrome resolves light-dark() in keyframes with the dark scheme. */
@property --air { syntax: "<number>"; inherits: true; initial-value: 0; }
@keyframes slots-air { 0% { --air: 1; } 25%, 100% { --air: 0; } }
@keyframes slots-on { 0% { opacity: 1; } 25%, 100% { opacity: 0; } }
@keyframes slots-off { 0% { opacity: 0; } 25%, 100% { opacity: 1; } }
.slots figcaption { margin-top: 1rem; font-size: 0.8rem; color: var(--muted); }
/* No motion: the tape stands still, and each radio shows its slot color. The speed has no use. */
@media (prefers-reduced-motion: reduce) {
  .slots * { animation: none !important; }
  .slots-speed { display: none; }
  .slots-radios > div { color: var(--c); border-color: var(--c); }
  .slots-radios span { display: none; }
}

/* The spectrum of the TETRA post (content/sdr-tetra-analyzer/server.js): a pixel band, hatched out of the span. Each
   carrier is a line on a peak of the noise. Control carriers stay on air with a blinking top pixel, traffic carriers
   come and go with the calls, and the noise changes in two frames (.pa and .pb of pixa.css). */
.spectrum :is(.hatch, .edge, .noise) { fill: var(--rule); }
.spectrum .zone { fill: var(--bg); }
.spectrum .mid { fill: none; stroke: var(--muted); stroke-dasharray: 2 2; }
/* A peak has the dark tone of its carrier color in a big palette: on a light page that tone is light. */
.spectrum .cpeak { fill: var(--c-blue-3, var(--rule)); }
.spectrum .tpeak { fill: var(--c-green-3, var(--rule)); }
.spectrum .ctl { fill: var(--c-blue); }
.spectrum .beacon { fill: var(--fg); }
.spectrum .call { animation: spectrum-call 4s steps(1, end) infinite; }
.spectrum .traffic { fill: var(--c-green); }
@keyframes spectrum-call { 60% { opacity: 0; } }

/* Palettes (data-palette on <html>, see pixa.css): each color that the styles above mix, fade or write as a hex
   value becomes a palette color. The gallery photo has a simple outline in place of the Polaroid frame. */
[data-palette] .card { padding: 0; color: var(--muted); background: none; border: 0; border-image: none; filter: none; }
[data-palette] .card::before { display: none; }
[data-palette] :is(.card img, .card.end p) { box-shadow: 0 0 0 2px var(--muted); }
[data-palette] .card.end p { background: var(--code-bg); color: var(--fg); }
[data-palette] .card figcaption { color: var(--muted); }
/* The light tones of the tab icons, and the steam, which fades with no mask. */
[data-palette] :is(.pot .shine, .pot .steam, .camera .mid, .eye .dim) { opacity: 1; fill: var(--muted); }
[data-palette] :is(.pot .burner, .camera .dim) { opacity: 1; fill: var(--rule); }
[data-palette] .pot [mask] { mask: none; }
/* The TETRA slots: a tone of the slot color, and a dither of the background on the past slots. */
[data-palette] .slots-tape i { background: var(--c3); }
[data-palette] .slots-air::before { background: repeating-conic-gradient(var(--bg) 0 25%, transparent 0 50%) 0 0 / 4px 4px; }
/* More token colors in code, so that a code block shows more colors of the palette. */
[data-palette] .hljs-title { color: var(--c-blue); }
[data-palette] .hljs-property { color: var(--c-teal); }
[data-palette] :is(.hljs-variable, .hljs-subst) { color: var(--c-purple); }
[data-palette] .hljs-number { color: var(--c-red); }
