/* ==========================================================================
   Monokai design system -- funwebfeatures.com
   ==========================================================================

   The palette is Monokai (Wimer Hazenberg's, by way of Sublime Text). It was
   designed for syntax at editor sizes, so two of its colors do not survive
   being used as interface text. Contrast ratios below are measured against
   the page background #272822.

     #F8F8F2  foreground   ~15.5:1   body text
     #A6E22E  green         ~9.6:1   safe anywhere
     #66D9EF  cyan          ~9.0:1   safe anywhere
     #FD971F  orange        ~6.8:1   safe anywhere
     #AE81FF  purple        ~5.2:1   passes AA
     #F92672  pink          ~3.9:1   FAILS AA at body size
     #75715E  comment gray  ~3.0:1   FAILS AA, worst on the palette

   Two rules follow, and they cover the two colors people reach for first:

   1. Pink is the signature Monokai accent. Headings, buttons, active states
      and other large or bold type only. Never paragraphs, never form labels.
   2. Comment gray is the natural pick for muted metadata and is the single
      worst performer here. It is available ONLY as a border and divider
      color. Secondary text uses --fg-muted (#A59F85, ~5.3:1) instead.

   Both rules are encoded in the token names below: --gray-chrome says what
   it is for, and there is no muted-text token that resolves to it.
   ========================================================================== */

:root {
  /* --- raw palette ------------------------------------------------------ */
  --mk-bg:        #272822;
  --mk-bg-alt:    #1E1F1C;
  --mk-surface:   #3E3D32;
  --mk-selection: #49483E;
  --mk-fg:        #F8F8F2;
  --mk-gray:      #75715E;
  --mk-pink:      #F92672;
  --mk-orange:    #FD971F;
  --mk-yellow:    #E6DB74;
  --mk-green:     #A6E22E;
  --mk-purple:    #AE81FF;
  --mk-cyan:      #66D9EF;

  /* Not canonical Monokai. Comment gray lifted until it clears AA for body
     text, keeping the hue so it still reads as part of the family. */
  --mk-gray-lift: #A59F85;

  /* --- semantic tokens -------------------------------------------------- */
  --bg:           var(--mk-bg);
  --bg-sunken:    var(--mk-bg-alt);
  --surface:      var(--mk-surface);
  --surface-hi:   var(--mk-selection);

  --fg:           var(--mk-fg);
  --fg-muted:     var(--mk-gray-lift);   /* secondary text -- NOT --mk-gray */
  --gray-chrome:  var(--mk-gray);        /* borders and dividers ONLY */

  --accent:       var(--mk-pink);        /* large/bold only */
  --accent-safe:  var(--mk-green);       /* when accent must carry small text */
  --link:         var(--mk-cyan);
  --link-hover:   var(--mk-green);

  --ok:           var(--mk-green);
  --warn:         var(--mk-orange);
  --info:         var(--mk-cyan);
  --special:      var(--mk-purple);

  --border:       #3E3D32;
  --border-hi:    var(--gray-chrome);

  /* --- type ------------------------------------------------------------- */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;

  --step--1: 0.8125rem;
  --step-0:  1rem;
  --step-1:  1.1875rem;
  --step-2:  1.5rem;
  --step-3:  2rem;
  --step-4:  2.75rem;

  /* --- space ------------------------------------------------------------ */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;
  --s-8: 4rem;

  --radius:    6px;
  --radius-lg: 10px;

  --shadow:    0 1px 2px rgb(0 0 0 / 0.3);
  --shadow-lg: 0 8px 24px rgb(0 0 0 / 0.4);

  --ring: 2px solid var(--mk-cyan);
  --ring-offset: 2px;

  --wrap: 1200px;
}

/* ==========================================================================
   Reset and base
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* Background lives on <html>, not <body>, so a fixed z-index:-1 canvas (the
   home-page shooting stars) can sit above it but behind the content. With the
   background on <body> that canvas would be painted over. <html> also fills
   the overscroll area, which <body> never did. */
html { -webkit-text-size-adjust: 100%; background: var(--bg); }

body {
  margin: 0;
  background: transparent;
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  margin: 0 0 var(--s-3);
  line-height: 1.2;
  font-weight: 700;
}

h1 { font-size: var(--step-4); letter-spacing: -0.02em; }
h2 { font-size: var(--step-3); letter-spacing: -0.01em; }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }

p { margin: 0 0 var(--s-4); }

a { color: var(--link); text-decoration-color: rgb(102 217 239 / 0.4); }
a:hover { color: var(--link-hover); text-decoration-color: currentColor; }

/* One visible focus style everywhere. Cyan clears 9:1 on every surface in
   this system, so it stays legible on cards and panels as well as the page. */
:focus-visible {
  outline: var(--ring);
  outline-offset: var(--ring-offset);
  border-radius: 3px;
}

img, iframe { max-width: 100%; }

hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: var(--s-6) 0;
}

code, kbd, pre { font-family: var(--font-mono); }

/* ==========================================================================
   Layout
   ========================================================================== */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--s-4);
}

.stack > * + * { margin-top: var(--s-4); }
.stack-lg > * + * { margin-top: var(--s-6); }

.row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
}

.push { margin-left: auto; }

/* ==========================================================================
   Type helpers
   ========================================================================== */

.muted     { color: var(--fg-muted); }
.small     { font-size: var(--step--1); }
.mono      { font-family: var(--font-mono); }
.nowrap    { white-space: nowrap; }

/* Pink is confined to large/bold type by rule. Applying .accent to something
   small is the one way to break the contrast floor, so it carries its own
   minimum size and weight rather than trusting the caller. */
.accent {
  color: var(--accent);
  font-weight: 700;
  font-size: max(var(--step-1), 1em);
}

.lede {
  font-size: var(--step-1);
  color: var(--fg-muted);
  max-width: 62ch;
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
  font: inherit;
  font-weight: 600;
  line-height: 1.4;
  text-decoration: none;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease,
              transform 120ms ease;
}

.btn:hover {
  background: var(--surface-hi);
  border-color: var(--fg-muted);
  color: var(--fg);
}

.btn:active { transform: translateY(1px); }

.btn[disabled], .btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

/* Pink on a pink-adjacent surface would be unreadable, so the primary button
   inverts: dark text on a pink fill. #272822 on #F92672 is the same ~3.9:1
   ratio read the other way, which is why the label is bold and 1.0625rem --
   large-text AA is 3:1 and this clears it. */
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--mk-bg);
  font-size: 1.0625rem;
  font-weight: 700;
}

.btn-primary:hover {
  background: #ff4a8d;
  border-color: #ff4a8d;
  color: var(--mk-bg);
}

/* When an action needs emphasis at small sizes, green carries it -- 9.6:1
   against the page and 6:1+ against every surface token here. */
.btn-go {
  background: transparent;
  border-color: var(--ok);
  color: var(--ok);
}

.btn-go:hover {
  background: rgb(166 226 46 / 0.12);
  border-color: var(--ok);
  color: var(--ok);
}

.btn-ghost {
  background: transparent;
  border-color: var(--border-hi);
  color: var(--fg-muted);
}

.btn-ghost:hover {
  background: var(--surface);
  color: var(--fg);
}

.btn-sm { padding: var(--s-1) var(--s-3); font-size: var(--step--1); }

/* ==========================================================================
   Tags and badges
   ========================================================================== */

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 2px var(--s-2);
  border: 1px solid var(--border-hi);
  border-radius: 999px;
  background: var(--bg-sunken);
  color: var(--fg-muted);
  font-size: var(--step--1);
  font-family: var(--font-mono);
  line-height: 1.5;
  text-decoration: none;
}

a.tag:hover { color: var(--link); border-color: var(--link); }

/* A permission badge is a trust signal, so it has to be noticeable without
   reading as an error. Orange clears 6.8:1 and is the only warm color here
   that does. */
.badge-perm {
  border-color: var(--warn);
  color: var(--warn);
  background: rgb(253 151 31 / 0.1);
}

.badge-clean {
  border-color: var(--ok);
  color: var(--ok);
  background: rgb(166 226 46 / 0.1);
}

.badge-fork {
  border-color: var(--special);
  color: var(--special);
  background: rgb(174 129 255 / 0.1);
}

/* ==========================================================================
   Surfaces
   ========================================================================== */

.panel {
  background: var(--surface);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius-lg);
  padding: var(--s-5);
}

.panel-sunken {
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s-4);
}

/* ==========================================================================
   Code
   ========================================================================== */

pre.code {
  margin: 0;
  padding: var(--s-4);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg);
  font-size: var(--step--1);
  line-height: 1.55;
  overflow-x: auto;
  tab-size: 2;
}

code.inline {
  padding: 0.1em 0.35em;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: 3px;
  font-size: 0.9em;
  color: var(--mk-yellow);
}

/* Syntax roles, used by the server-side highlighter for embed snippets and
   the view-source panel. Every one of these is >= 5:1 on --bg-sunken. */
.tok-tag     { color: var(--mk-pink); }
.tok-attr    { color: var(--mk-green); }
.tok-string  { color: var(--mk-yellow); }
.tok-value   { color: var(--mk-purple); }
.tok-key     { color: var(--mk-cyan); }
.tok-comment { color: var(--fg-muted); font-style: italic; }
.tok-punct   { color: var(--fg-muted); }

/* ==========================================================================
   Forms
   ========================================================================== */

.field { display: block; margin-bottom: var(--s-4); }

/* Labels are small text, so this token must never become pink or gray. */
.field-label {
  display: block;
  margin-bottom: var(--s-2);
  color: var(--fg-muted);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.input, .select, .textarea {
  width: 100%;
  padding: var(--s-2) var(--s-3);
  background: var(--bg-sunken);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  color: var(--fg);
  font: inherit;
  font-size: var(--step-0);
}

.input:hover, .select:hover, .textarea:hover { border-color: var(--fg-muted); }

.input::placeholder, .textarea::placeholder { color: var(--gray-chrome); }

.range {
  width: 100%;
  accent-color: var(--mk-pink);
  background: transparent;
}

.color-input {
  width: 100%;
  height: 2.5rem;
  padding: 2px;
  background: var(--bg-sunken);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  cursor: pointer;
}

/* ==========================================================================
   Gadget frame
   ==========================================================================
   Gadgets are cross-origin iframes carrying their own styling, dropped onto
   a dark page. A gadget with a white background would glow like a lightbulb
   in the grid, so the manifest declares background: transparent | light |
   dark and the frame adapts rather than the gadget having to.
   ========================================================================== */

.gadget-frame {
  position: relative;
  display: block;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-sunken);
}

.gadget-frame > iframe {
  display: block;
  width: 100%;
  border: 0;
  background: transparent;
}

/* Declared transparent: the gadget inherits the page. Nothing to do. */
.gadget-frame[data-bg="transparent"] { background: var(--bg-sunken); }

/* Declared light: give it a light mat so the edge is intentional instead of
   a glowing rectangle butting straight against a dark card. */
.gadget-frame[data-bg="light"] {
  background: #F5F5EF;
  border-color: var(--gray-chrome);
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.08);
}

.gadget-frame[data-bg="dark"] {
  background: #14150F;
  border-color: var(--border);
}

/* Loading state.
 *
 * A gadget reports its real height only after it loads, so revealing frames
 * immediately means every card visibly grows or shrinks under the reader --
 * a whole gallery of them lurching at once. Instead: hold the declared size,
 * show a spinner, let the height settle while the gadget is still hidden,
 * and fade it in once it is done moving. */
.gadget-frame[data-loading="true"] > iframe {
  opacity: 0;
}

.gadget-frame > iframe {
  opacity: 1;
  transition: opacity 220ms ease;
}

.gadget-frame[data-loading="true"]::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border-radius: 50%;
  border: 2px solid var(--border-hi);
  border-top-color: var(--accent);
  animation: gadget-spin 700ms linear infinite;
  pointer-events: none;
  z-index: 1;
}

@keyframes gadget-spin { to { transform: rotate(360deg); } }

/* A spinner is exactly the kind of thing motion sensitivity is about. Pulse
   the ring instead of rotating it -- still clearly "working". */
@media (prefers-reduced-motion: reduce) {
  .gadget-frame[data-loading="true"]::before {
    animation: gadget-pulse 1.4s ease-in-out infinite;
    border-color: var(--accent);
  }
  @keyframes gadget-pulse {
    0%, 100% { opacity: 0.25; }
    50%      { opacity: 0.9; }
  }
}

.gadget-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.03);
}

/* ==========================================================================
   Cards and the gallery grid
   ========================================================================== */

/* Masonry, via multi-column.
 *
 * Gadgets declare wildly different heights -- a 90px pill beside a 380px
 * receipt -- so a row-based grid either stretches short cards into mostly
 * empty boxes or leaves ragged horizontal bands. Columns pack vertically
 * instead, Pinterest style.
 *
 * Why not `grid-template-rows: masonry`: still not interoperably shipping,
 * and the CSSWG has been weighing replacing that syntax outright, so it is
 * not something to build on yet. Why not JS masonry: our gadget iframes
 * report their height asynchronously after load, which would mean recomputing
 * positions on every message. Multi-column reflows on its own when a card
 * changes height, which is exactly the behaviour we need and none of the
 * bookkeeping.
 *
 * The tradeoff: cards flow top-to-bottom within a column, so visual order is
 * column-major rather than left-to-right. Acceptable here -- the gallery is
 * an unordered set (alphabetical, no ranking yet), and DOM order still drives
 * tab and screen-reader order. Revisit if ranking ever makes position mean
 * something. */
/* Row-major masonry, via a fine-grained grid.
 *
 * Every card spans however many 8px rows its content needs (frames.js
 * measures and sets grid-row-end). The result packs vertically like masonry
 * but fills LEFT TO RIGHT, so position in the list maps to position on the
 * wall -- which is what makes "put this one beside that one" a thing you can
 * actually ask for. Plain `columns` fills top-to-bottom per column instead,
 * where neighbours in the list end up stacked rather than side by side.
 *
 * Without JS the spans never get set, every card occupies one 8px row and
 * they overlap -- so the fallback below keeps it a plain readable grid until
 * frames.js marks the gallery ready. */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  column-gap: var(--s-5);
  row-gap: var(--s-5);
  align-items: start;
  padding: 0;
  margin: 0;
  list-style: none;
}

.gallery.masonry {
  grid-auto-rows: 8px;
  row-gap: 0;               /* spacing comes from each card's margin instead */
}

/* NOTE: do not reach for `column-span: all` to feature a wide gadget. It
   terminates the column flow, so every spanning card splits the masonry into
   separate blocks and strands the cards around it beside a void. Tried it,
   it wrecked the wall. A gadget too wide for a column is the gadget's
   problem to solve -- gadgets are iframes and get a real viewport, so they
   can and should be responsive. */

.gallery > .card {
  display: block;
  align-self: start;
}

/* Once spans are being set, row-gap is 0 and this margin does the spacing --
   it is included in the measured height so the maths stays honest. */
.gallery.masonry > .card { margin-bottom: var(--s-5); }

/* ---- animated divider waveforms ---------------------------------------
   One strip per card, sitting in the gap below it, so vertically stacked
   cards get a moving line between them. Built entirely by waves.js -- the
   per-strip colour, speed and direction are set through the CSSOM because
   our CSP forbids inline style attributes. */

.card-wave {
  position: absolute;
  left: 0;
  right: 0;
  /* Taller than the gap it sits in, reaching a few px up into the card's
     bottom padding. The gap alone is a 24px hit target, which is under the
     ~44px comfortable minimum and easy to miss. The padding is empty space,
     so nothing interactive gets covered. */
  bottom: calc(var(--s-5) * -1 - 6px);
  height: calc(var(--s-5) + 12px);
  display: block;
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
  overflow: hidden;
  cursor: pointer;
  z-index: 1;
}

/* MUST come after the rule above.
 *
 * `display: block` on .card-wave outranks the UA stylesheet's built-in
 * `[hidden] { display: none }` -- author styles beat UA styles -- so setting
 * el.hidden = true marked the element hidden without actually hiding it.
 * The column-tail waves stayed on screen while every check that read the
 * .hidden property reported them correctly hidden. Any element you plan to
 * toggle with `hidden` needs this whenever you give it a display value. */
.card-wave[hidden] { display: none; }

/* The BUTTON is taller than the gap so it is comfortable to click, but the
   drawn wave must stay inside the gap -- at full height it overhangs into
   the top edge of the card below and stops reading as "between" the two.
   So: inset the svg to exactly the gap, centred in the taller hit area. */
.card-wave svg {
  display: block;
  width: 100%;
  height: var(--s-5);
  margin-top: 6px;
}

/* Heart-monitor sweep.
 *
 * The trace itself never moves -- only the drawing head does. With
 * pathLength normalised to 1 and a "1 on, 1 off" dash pattern, animating
 * stroke-dashoffset from 1 to -1 walks the visible window across the path:
 * 1 -> 0 draws it in from the left, 0 -> -1 erases it from the left. One
 * continuous linear loop, no seam, and it works for any waveform. */
.card-wave path {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1 1;
  animation: ecg-sweep 4s linear infinite;
}

@keyframes ecg-sweep {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: -1; }
}

/* Hovering a card freezes the divider beneath it -- the sweep and, if it is
 * shimmering, the colour drift too, so the whole line holds still rather
 * than half-stopping. animation-play-state pauses in place; it does not
 * reset, so leaving the card resumes exactly where it stopped.
 *
 * :focus-within gives keyboard users the same behaviour when they tab into
 * a card. */
.card:hover .card-wave path,
.card:focus-within .card-wave path,
.card:hover .card-wave stop,
.card:focus-within .card-wave stop {
  animation-play-state: paused;
}

.card-wave:focus-visible {
  outline: 2px solid var(--mk-cyan);
  outline-offset: 2px;
  border-radius: 4px;
}

/* No sweeping under reduced motion. Show the finished trace instead of a
   half-drawn one -- dashoffset 0 is the fully drawn state. */
@media (prefers-reduced-motion: reduce) {
  .card-wave path {
    animation: none;
    stroke-dashoffset: 0;
  }
  /* Freeze the shimmer mid-palette so it stays a gradient, not a flat line. */
  .card-wave stop { animation-play-state: paused; }
}

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius-lg);
  /* NOT overflow:hidden. That was here to clip the iframe's square corners
     to the card's rounded ones, but it also clips the wave strip, which
     lives in the gap BELOW the card. The clipping moved to the frame. */
  transition: border-color 140ms ease, transform 140ms ease,
              box-shadow 140ms ease;
}

.card:hover {
  border-color: var(--fg-muted);
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
}

.card:focus-within {
  border-color: var(--mk-cyan);
}

/* The frame does its own corner clipping now, so the card can let the wave
   strip hang past its bottom edge. */
.card > .gadget-frame {
  border: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  border-bottom: 1px solid var(--border-hi);
  overflow: hidden;
}

.card-body {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-4);
}

.card-title {
  margin: 0;
  font-size: var(--step-1);
  font-weight: 700;
  line-height: 1.25;
}

/* The card title is the one place pink earns its keep: large, bold, and the
   thing the eye should land on first. */
.card-title a {
  color: var(--accent);
  text-decoration: none;
}

.card-title a:hover { text-decoration: underline; }

.card-tagline {
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--step--1);
}

/* No margin-top:auto any more -- cards hug their content under masonry, so
   there is no leftover space to push the meta row into. */
.card-meta {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding-top: var(--s-3);
}

/* ==========================================================================
   Page utilities
   ==========================================================================
   These exist because our own CSP is style-src 'self', which blocks inline
   style attributes. That is deliberate and worth keeping: the alternative is
   'unsafe-inline', which would also re-enable every injected style attribute
   an XSS could plant. So layout lives here in classes, not in the markup.
   ========================================================================== */

.page       { padding-block: var(--s-7); }
.page-tight { padding-block: var(--s-6); }

.measure       { max-width: 68ch; }
.measure-wide  { max-width: 72ch; }

.mt-3 { margin-top: var(--s-3); }
.mt-4 { margin-top: var(--s-4); }
.mt-6 { margin-top: var(--s-6); }
.mt-8 { margin-top: var(--s-8); }
.mb-4 { margin-bottom: var(--s-4); }
.mb-5 { margin-bottom: var(--s-5); }
.mb-7 { margin-bottom: var(--s-7); }
.flush { margin: 0; }

/* --- listing page: demo beside its controls ------------------------------ */

.gadget-title {
  font-size: var(--step-3);
  color: var(--accent);
}

.panel-title {
  font-size: var(--step-1);
  margin-bottom: var(--s-3);
}

.panel-title-lg { margin-bottom: var(--s-4); }

.frame-note { margin: var(--s-2) 0 0; }

.source-view {
  margin-top: var(--s-3);
  max-height: 60vh;
  overflow: auto;
}

.warn-note { color: var(--warn); margin: var(--s-3) 0 0; }

.clickable { cursor: pointer; }

/* --- error page ---------------------------------------------------------- */

.error-code {
  font-family: var(--font-mono);
  font-size: var(--step-4);
  color: var(--accent);
  margin: 0;
}

.error-title { font-size: var(--step-2); }


.demo-layout {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
  align-items: start;
}

@media (max-width: 860px) {
  .demo-layout { grid-template-columns: minmax(0, 1fr); }
}

.demo-layout output {
  display: inline-block;
  margin-top: var(--s-1);
}

details > summary { color: var(--fg-muted); }
details > summary:hover { color: var(--fg); }

/* ==========================================================================
   Site chrome
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgb(39 40 34 / 0.92);
  backdrop-filter: blur(8px);
  /* No border-bottom: the wave below IS the divider. A rule plus a wave
     reads as two separators stacked. */
  padding-bottom: 2px;
}

/* Header divider, sweeping on the same keyframe as the card dividers so the
   whole page speaks one visual language. Pure markup + CSS -- the path is
   baked into the partial, so this needs no JS and works on every page,
   including ones waves.js does not run on. */
.header-wave {
  display: block;
  width: 100%;
  max-width: var(--wrap);
  height: 16px;
  margin: 0 auto;
  padding-inline: var(--s-4);
  box-sizing: border-box;
  overflow: visible;
}

/* Palette drift.
 *
 * Each gradient stop walks the full Monokai palette; a negative, staggered
 * delay puts every stop at a different point in that walk, so at any instant
 * the line holds several colours blending into each other, and over time they
 * travel along it. stop-color is CSS-animatable, so no SMIL and no JS. */
@keyframes wave-hue {
  0%   { stop-color: #F92672; }
  17%  { stop-color: #FD971F; }
  34%  { stop-color: #E6DB74; }
  50%  { stop-color: #A6E22E; }
  67%  { stop-color: #66D9EF; }
  84%  { stop-color: #AE81FF; }
  100% { stop-color: #F92672; }
}

.header-wave stop { animation: wave-hue 18s linear infinite; }
.header-wave stop:nth-child(1) { animation-delay:   0s; }
.header-wave stop:nth-child(2) { animation-delay:  -3s; }
.header-wave stop:nth-child(3) { animation-delay:  -6s; }
.header-wave stop:nth-child(4) { animation-delay:  -9s; }
.header-wave stop:nth-child(5) { animation-delay: -12s; }
.header-wave stop:nth-child(6) { animation-delay: -15s; }

.header-wave path {
  fill: none;
  stroke: url(#fwf-wave);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  opacity: 0.7;
  stroke-dasharray: 1 1;
  animation: ecg-sweep 9s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .header-wave path { animation: none; stroke-dashoffset: 0; }
  /* Freeze the palette too, but leave the stops on different colours so it
     is still a gradient rather than a flat line. */
  .header-wave stop { animation-play-state: paused; }
}

.site-header .wrap {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  height: 60px;
}

.brand {
  display: inline-flex;
  align-items: baseline;
  gap: 0;
  font-family: var(--font-mono);
  font-size: var(--step-1);
  font-weight: 700;
  text-decoration: none;
  letter-spacing: -0.02em;
}

.brand-fun  { color: var(--mk-pink); }
.brand-web  { color: var(--mk-green); }
.brand-feat { color: var(--mk-cyan); }
.brand-tld  { color: var(--fg-muted); font-weight: 400; }

.nav { display: flex; align-items: center; gap: var(--s-4); }

.nav a {
  color: var(--fg-muted);
  font-size: var(--step--1);
  font-weight: 600;
  text-decoration: none;
}

.nav a:hover, .nav a[aria-current="page"] { color: var(--fg); }

.site-footer {
  margin-top: var(--s-8);
  padding: var(--s-6) 0;
  border-top: 1px solid var(--border);
  color: var(--fg-muted);
  font-size: var(--step--1);
}

.footer-lead {
  margin: 0 0 var(--s-3);
  max-width: 68ch;
}

.footer-legal {
  gap: var(--s-2);
  padding-top: var(--s-3);
  border-top: 1px solid var(--border);
}

.footer-sep { color: var(--gray-chrome); }

/* The cursor-trail off switch. Quiet by default -- it should be findable by
   anyone who wants it gone, without advertising itself to everyone else. */
.trail-toggle {
  padding: 2px var(--s-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--gray-chrome);
  font: inherit;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  cursor: pointer;
}

.trail-toggle:hover { color: var(--fg-muted); border-color: var(--fg-muted); }
.trail-toggle[aria-pressed="true"] { color: var(--fg-muted); }

/* ==========================================================================
   Motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .card:hover { transform: none; }
}

/* Monokai is a dark theme by definition, so there is no light mode to switch
   into. Forced-colors mode still has to work, and that means not relying on
   our own borders to separate surfaces. */
@media (forced-colors: active) {
  .card, .panel, .gadget-frame, .btn { border: 1px solid ButtonBorder; }
  .btn-primary { forced-color-adjust: none; }
}

/* ── Pixboard ad rail (pixboard-rail SDK) ─────────────────────────────────
   Dock the per-publisher element on the LEFT, reserve a matching gutter at the
   rail's 700px breakpoint so content never underlaps it, and theme the hover
   card dark to match. The element tag is "px-" + the PIXBOARD_PATH prefix. */
px-ebd830ff43644bf3 {
  position: fixed; left: 10px; top: 50%; transform: translateY(-50%); z-index: 1030;
}
@media (min-width: 700px) { body { padding-left: 60px; } }
:root {
  --pxr-card-bg: #211f1a;
  --pxr-card-fg: #F8F8F2;
  --pxr-card-border: 1px solid rgba(248, 248, 242, .16);
  --pxr-card-radius: 14px;
  --pxr-card-shadow: 0 18px 50px rgba(0, 0, 0, .55);
}
