/* The design.
   Was generated from the prototype's one <style> block until the theme became
   the product; it is hand-authored from here, and the tool that lifted it is
   gone. Everything is scoped to #site (the prototype called that wrapper #vb).
   wp.css loads after this file and holds the rules that only make sense once
   WordPress is drawing the page - it is a companion, not an override layer, so
   a rule about how the site looks belongs here. */

:root{ color-scheme:light; --orange:#dc4c00; --ember:#f56c27; --rust:#b83d00; --cream:#f1e6c8; --teal:#82c0c7; }
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0; background:#fffdf8}

:root[data-theme="dark"]{color-scheme:dark}
:root[data-theme="dark"] body{background:#17130e}
img{max-width:100%; display:block}
/* The line above is an author rule, so it outranks the UA sheet's
   [hidden]{display:none} and leaves a hidden <img> on the page taking up its
   box. Every img here is display:block deliberately; this is the exception the
   rule forgot. */
img[hidden]{display:none}
button{font:inherit; color:inherit}
:focus-visible{outline:2px solid var(--ember); outline-offset:2px; border-radius:3px}

/* ═══════════════════════════════════════════════════════════════
   MUDLET — direction B, refined
   ═══════════════════════════════════════════════════════════════ */
#site{
  --bg:#fffdf8; --band:#faf3e4; --ink:#211d17; --dim:#6f6656; --line:#e9dfc8;
  --ink-d:#17130e; --ink-d2:#211b14; --line-d:#362c21; --on-d:#e8dcc6; --on-d-dim:#9c8f7c;
  /* accent — the brand orange retuned for contrast. #dc4c00 gives white text
     only 4.14:1 (needs 4.5) and 3.74:1 as link text on cream, which is why it
     read as muddy; #c44400 clears both. */
  --orange:#c44400; --rust:#a83800; --mark:#c44400;
  --link:#a83800; --hl:#c44400; --accent2:#c44400;
  --glow:rgba(196,68,0,.45);
  background:var(--bg); color:var(--ink); font-size:1rem; line-height:1.62;
  -webkit-font-smoothing:antialiased;
  /* surfaces, so the whole page can be repainted from one block */
  --card:#ffffff; --hover:rgba(255,255,255,.6); --topbg:rgba(255,253,248,.93);
  --on-accent:#ffffff; --sh:60,40,10;
  /* One vertical rhythm for every band on the page. It used to be typed into
     nine rules in three barely-different clamps — 7vw here, 6vw there, a 4rem
     cap on one and 4.5rem on the next — differences nobody could see and
     nobody was maintaining. This is that value, unified and halved: the old
     spacing was drawn for a page of three sections and reads as dead air on
     one with six. Sections that genuinely want more room say so themselves. */
  --secpad:clamp(1.5rem,3.5vw,2.25rem);
}

/* ── DARK ─────────────────────────────────────────────────────────
   Warm, not neutral: the same parchment hues turned down rather than
   a grey theme wearing an orange hat. Ratios against --card #241d15
   are body 12.74:1, dim 5.61:1, accent 5.59:1. The accent inverts —
   #f56c27 is too bright to carry white text (2.98:1), so the button
   takes dark text on bright orange instead (6.21:1).
   --ink-d/--ink-d2 drive the "hop in" band and footer, which were the
   dark slab in a light page; here they become the raised one, so they
   are lifted ABOVE --bg rather than sunk below it. */
:root[data-theme="dark"] #site{
  --bg:#17130e; --band:#1e1811; --card:#241d15;
  --ink:#ece0c9; --dim:#a2947e; --line:#332a1e;
  --ink-d:#221b13; --ink-d2:#2b2318; --line-d:#3a2f22; --on-d:#ece0c9; --on-d-dim:#a2947e;
  --orange:#f56c27; --rust:#ff8a4d; --mark:#f56c27;
  --link:#f56c27; --hl:#f56c27; --accent2:#f56c27;
  --on-accent:#17130e;
  --glow:rgba(245,108,39,.3);
  --hover:rgba(255,255,255,.05); --topbg:rgba(23,19,14,.93);
  --sh:0,0,0;
}
/* game logos are drawn for a light ground, so their tile keeps the
   cream in both themes rather than swallowing half the artwork */
/* type: locked to Plex Mono headings + Figtree body */
#site{ --display:'IBM Plex Mono',ui-monospace,monospace; --body:'Figtree',system-ui,sans-serif;
  --dw:600; --dls:-.028em; --dlh:1.14 }

#site{font-family:var(--body)}
#site h1,#site h2,#site h3{margin:0; font-family:var(--display);
  font-weight:var(--dw); letter-spacing:var(--dls); line-height:var(--dlh)}
#site p{margin:0}
#site a{color:inherit}
#site .w{max-width:74rem; margin-inline:auto; padding-inline:clamp(1.1rem,4vw,2rem)}

/* the > mark, carried through as a brand device */
#site .mk{color:var(--mark); font-family:'IBM Plex Mono',ui-monospace,monospace; font-weight:600}

#site .top{position:sticky; top:0; z-index:80; background:var(--topbg);
  backdrop-filter:blur(12px); border-bottom:1px solid var(--line)}
#site .top .w{display:flex; align-items:center; gap:1rem; height:3.7rem}
#site .logo{display:flex; align-items:center; gap:.5rem; font-family:var(--display);
  font-weight:var(--dw); font-size:1.1rem; letter-spacing:-.035em; text-decoration:none}
#site .logo img{width:1.7rem; height:1.7rem}
/* Places to read; Download is not among them, because the button already is
   the download. Two controls pointing at /download/ half a centimetre apart
   read as one control drawn twice. */
/* The nav and the utilities share one wrapper so the narrow-screen drawer can
   be them, rather than a second copy of them. In the bar the wrapper is not a
   box at all - display:contents - so .topnav and .util remain flex items of
   .w and everything below is the layout it always was. */
#site .menu{display:contents}
#site .topnav{margin-left:auto; display:flex; gap:.25rem; align-items:center}
#site .topnav a{font-size:.9rem; font-weight:500; text-decoration:none; color:var(--dim);
  padding:.4rem .65rem; border-radius:7px}
#site .topnav a:hover{color:var(--ink); background:var(--band)}
/* A nav item with children. The button is the anchor above with a caret
   bolted on - same size, same padding, same states - because it sits in the
   same row and any difference between them would read as a mistake. The panel
   is the language menu's, hung off the left edge instead of the right: these
   are in the middle of the bar, and one opening rightwards from there would
   walk off the end of a narrow window. */
#site .nav__grp{position:relative; display:flex}
#site .nav__top{display:flex; align-items:center; gap:.3rem; font-family:inherit;
  font-size:.9rem; font-weight:500; color:var(--dim); background:transparent;
  border:0; padding:.4rem .65rem; border-radius:7px; cursor:pointer}
#site .nav__top:hover,#site .nav__top[aria-expanded="true"]{color:var(--ink); background:var(--band)}
#site .nav__top .crt{width:.7rem; height:.7rem; opacity:.7}
#site .nav__sub{position:absolute; left:0; top:calc(100% + .5rem); z-index:5;
  margin:0; padding:.3rem; list-style:none; min-width:11rem; background:var(--card);
  border:1px solid var(--line); border-radius:10px; box-shadow:0 18px 34px -18px rgba(var(--sh),.5)}
#site .nav__sub[hidden]{display:none}
/* the rest of the row is .topnav a already; these two are what a panel adds */
#site .nav__sub a{display:block; white-space:nowrap}
/* The row either fits or it is a drawer; there is no middle state where the
   bar keeps some of it. Measured, the whole header - logo, seven items, the
   utilities and the button - needs about 61rem, so it folds at 64rem, which
   is also the one breakpoint the drawer below opens at. Two breakpoints here
   is what left a window 52-64rem wide showing "News" and nothing else, with
   no way to reach the rest. */

/* — utilities: search, language, the one call to action — */
#site .util{display:flex; align-items:center; gap:.45rem; padding-left:.85rem;
  border-left:1px solid var(--line)}
/* — search: an icon in the bar, a command palette on top — */
#site .searchbtn{display:flex; align-items:center; justify-content:center; width:2.05rem; height:2.05rem;
  color:var(--dim); background:transparent; border:1px solid transparent; border-radius:7px; cursor:pointer}
#site .searchbtn:hover{color:var(--ink); background:var(--band)}
#site .searchbtn svg{width:1.05rem; height:1.05rem}

/* 34rem of height, not the 28 this opened at: the list is two blocks now — the
   site's rows and the wiki's — and each ends in a row out of it. At 28rem the
   panel held eight rows, which is the site's half and a glimpse of the wiki's.
   The calc still wins on a short window, and the list still scrolls. */
#site .palette{width:min(34rem,calc(100vw - 2rem)); max-height:min(34rem,calc(100vh - 8rem));
  margin:11vh auto auto; padding:0; overflow:hidden; color:var(--ink);
  background:var(--card); border:1px solid var(--line); border-radius:14px;
  box-shadow:0 40px 80px -28px rgba(var(--sh),.6)}
#site .palette::backdrop{background:rgba(0,0,0,.45); backdrop-filter:blur(2px)}
#site .palette[open]{display:flex; flex-direction:column; animation:palin .16s ease both}
@keyframes palin{ from{opacity:0; transform:translateY(-8px)} to{opacity:1; transform:none} }
@media (prefers-reduced-motion:reduce){ #site .palette[open]{animation:none} }

#site .palette__in{display:flex; align-items:center; gap:.7rem; margin:0;
  padding:.9rem 1rem; border-bottom:1px solid var(--line)}
#site .palette__in svg{width:1.05rem; height:1.05rem; color:var(--dim); flex:none}
#site .palette__in input{flex:1; min-width:0; font:inherit; font-size:1rem; color:var(--ink);
  background:transparent; border:0; outline:none; padding:0}
#site .palette__in input::placeholder{color:var(--dim)}
#site .palette kbd{font-family:var(--display); font-size:.68rem; color:var(--dim);
  background:var(--band); border:1px solid var(--line); border-radius:4px; padding:.05rem .34rem}

#site .palette__list{flex:1; overflow-y:auto; margin:0; padding:.35rem; list-style:none}
#site .palette__list button{display:flex; align-items:baseline; gap:.75rem; width:100%; text-align:left;
  font-family:var(--body); font-size:.9rem; background:transparent; border:0; cursor:pointer;
  padding:.5rem .65rem; border-radius:8px}
#site .palette__list button .src{margin-left:auto; font-family:var(--display); font-size:.68rem;
  letter-spacing:.06em; text-transform:uppercase; color:var(--dim); flex:none}
/* one highlight, driven by the keyboard — hover just mirrors it */
#site .palette__list button[aria-selected="true"]{background:var(--band)}
#site .palette__list button[aria-selected="true"] .t{color:var(--link)}
/* The last row: not a result, a way out to the whole list of them. Sticky
   rather than simply last, because the results scroll and an offer you have to
   scroll to find is one nobody takes. */
#site .palette__all{position:sticky; bottom:-.35rem; margin-top:.35rem;
  padding:.35rem 0 .35rem; background:var(--card); border-top:1px solid var(--line)}
#site .palette__all button .t{color:var(--dim)}
#site .palette__all button[aria-selected="true"] .t{color:var(--link)}
/* The wiki's block: a rule where the site's results end, and its own way out at
   the bottom of it. That one is not sticky — a single row can hold the bottom
   edge, and the site's results own it. */
#site .palette__group{margin-top:.35rem; padding-top:.35rem; border-top:1px solid var(--line)}
#site .palette__out button .t{color:var(--dim)}
#site .palette__out button[aria-selected="true"] .t{color:var(--link)}
#site .palette__empty{padding:1.6rem 1rem; text-align:center; color:var(--dim); font-size:.9rem}
#site .palette__empty[hidden]{display:none}
#site .palette__foot{display:flex; gap:1.1rem; padding:.55rem 1rem; font-size:.72rem; color:var(--dim);
  border-top:1px solid var(--line); background:var(--band)}
#site .palette__foot span{display:flex; align-items:center; gap:.35rem}

/* — the lightbox: the screenshot carousel's full-size view —
   The second <dialog> on the site, and for the same reasons as the first:
   showModal() brings the top layer, the backdrop, the focus trap, the
   inert-page behaviour and Escape without any of it being written here.
   It is built by assets/js/theme.js and appended inside #site, so it reads
   the same custom properties as everything else and follows the theme
   toggle; a dialog parked on <body> would come up in light mode on a dark
   page. Dark-first regardless of theme, though — a picture is what the
   visitor came to look at, and a cream sheet around it is a second thing
   competing with it. */
#site .mlb{width:100vw; max-width:none; height:100dvh; max-height:none; margin:0; padding:0;
  background:transparent; border:0; color:#f3ece0; overflow:hidden}
#site .mlb::backdrop{background:rgba(10,7,4,.92)}
/* Two rows, not three: the bar is fixed like the arrows, because it is chrome
   over the picture rather than a band above it — which is also what keeps the
   close button in the corner of the screen where a visitor reaches for it in
   both of the shapes below. */
#site .mlb[open]{display:grid; grid-template-rows:minmax(0,1fr) auto;
  padding-top:3.6rem; animation:mlbin .18s ease both}
/* A picture is given the room and fills it. A video is a fixed shape that
   cannot use the room, so the frame and its caption shrink to the video and
   centre together — otherwise the player floats in the middle of a black field
   with its own caption stranded an inch off the bottom of the screen. */
#site .mlb[open][data-kind="video"]{grid-template-rows:auto auto; align-content:center;
  padding-top:0}
@keyframes mlbin{ from{opacity:0} to{opacity:1} }
@media (prefers-reduced-motion:reduce){ #site .mlb[open]{animation:none} }

#site .mlb__bar{position:fixed; top:0; left:0; right:0; z-index:2;
  display:flex; align-items:center; gap:1rem; padding:.7rem .9rem}
#site .mlb__n{margin-right:auto; font-family:var(--display); font-size:.75rem;
  letter-spacing:.06em; color:#a2947e; font-variant-numeric:tabular-nums}
/* Dark and solid rather than a light film, because these sit *on* the picture
   and the picture is somebody's screenshot: a white Mudlet window one slide, a
   black terminal the next. A 6%-white ground disappears entirely over the
   second, which is a next button nobody can find. Dark with a bright rim reads
   over both. */
#site .mlb__btn{display:grid; place-items:center; width:2.4rem; height:2.4rem; padding:0;
  color:#f3ece0; background:rgba(16,11,6,.62); border:1px solid rgba(255,255,255,.3);
  border-radius:50%; cursor:pointer; backdrop-filter:blur(3px);
  box-shadow:0 2px 10px rgba(0,0,0,.45);
  transition:background .15s, border-color .15s}
#site .mlb__btn:hover{background:rgba(16,11,6,.88); border-color:rgba(255,255,255,.6)}
#site .mlb__btn[hidden]{display:none}
#site .mlb__btn svg{width:1.15rem; height:1.15rem}

/* The figure is the middle row and owns no height of its own: min-height:0 on
   a grid item is what lets max-height:100% mean the row rather than the
   dialog, which is the difference between a tall screenshot fitting and it
   pushing the caption off the bottom of the screen. */
#site .mlb__fig{position:relative; min-height:0; margin:0; padding:0}
#site .mlb[data-kind="video"] .mlb__fig{display:block}
/* Absolutely positioned, and that is the whole trick. Every percentage way of
   writing "as big as the row and no bigger" fails here: the figure is a grid
   item stretched into a 1fr row, and Chrome will not treat that height as
   definite for a replaced child, so both height:100% and max-height:100% come
   out as auto and a tall screenshot renders at its natural size and runs off
   the bottom of the screen. An absolutely positioned box takes its size from
   the containing block directly, with no percentage to resolve, and object-fit
   shrinks the picture inside it. The 1rem inset is the breathing room the
   figure's padding used to give. */
#site .mlb__fig img{position:absolute; inset:0 1rem; width:calc(100% - 2rem); height:100%;
  object-fit:contain; border-radius:10px}
/* Sat over the picture, not beside it: the arrows are for moving through
   fifteen shots quickly, and a hand that has found one should not have to
   travel to find the other. */
#site .mlb__arrow{position:fixed; top:50%; margin-top:-1.4rem; z-index:1;
  width:2.8rem; height:2.8rem}
#site .mlb__arrow--prev{left:max(.9rem,2vw)}
#site .mlb__arrow--next{right:max(.9rem,2vw)}
/* In the flow, unlike the picture, and that is the point: the row above is
   sized to whatever this comes out as. Sized off the viewport rather than off
   its parent for the same reason the picture is — an iframe is a replaced
   element too, and the percentage would not resolve — so it is a 16:9 box
   fitted to whichever runs out first, the width of the dialog or the height
   left once the bar and the caption have taken theirs. */
#site .mlb__frame{display:block; margin-inline:auto; border:0; border-radius:10px;
  width:min(calc(100% - 2rem), calc((100dvh - 11rem) * 16 / 9)); aspect-ratio:16/9; height:auto;
  background:#000}
#site .mlb__frame[hidden]{display:none}
#site .mlb__cap{min-height:2.6rem; padding:.8rem 1.2rem 1.2rem; text-align:center;
  font-family:var(--display); font-size:.8rem; line-height:1.6; color:#a2947e}
#site .mlb__out{margin-left:.9ch; color:#f3ece0; text-underline-offset:.2em}
#site .mlb__out[hidden]{display:none}
#site .mlb__out:hover{color:var(--ember)}
@media (max-width:44rem){
  /* At phone width there is no room beside the picture for a 2.8rem circle,
     so the arrows come out of the overlay and join the bar at the top, where
     the close button already is. */
  #site .mlb__arrow{position:static; margin:0; width:2.4rem; height:2.4rem}
  #site .mlb__fig{padding-inline:.5rem}
}

#site .theme{display:flex; align-items:center; justify-content:center; width:2.05rem; height:2.05rem;
  color:var(--dim); background:transparent; border:1px solid transparent; border-radius:7px; cursor:pointer}
#site .theme:hover{color:var(--ink); background:var(--band)}
#site .theme svg{width:1rem; height:1rem}
/* show the theme you would switch TO, not the one you are in */
#site .theme .sun{display:none}
:root[data-theme="dark"] #site .theme .sun{display:block}
:root[data-theme="dark"] #site .theme .moon{display:none}

#site .lang{position:relative}
#site .lang__btn{display:flex; align-items:center; gap:.3rem; font-family:var(--display);
  font-size:.8rem; font-weight:600; color:var(--dim); background:transparent;
  border:1px solid transparent; padding:.42rem .5rem; border-radius:7px; cursor:pointer}
#site .lang__btn:hover,#site .lang__btn[aria-expanded="true"]{color:var(--ink); background:var(--band)}
#site .lang__btn .glb{width:.95rem; height:.95rem}
#site .lang__btn .crt{width:.7rem; height:.7rem; opacity:.7}
#site .lang__menu{position:absolute; right:0; top:calc(100% + .5rem); z-index:5;
  margin:0; padding:.3rem; list-style:none; min-width:10rem; background:var(--card);
  border:1px solid var(--line); border-radius:10px; box-shadow:0 18px 34px -18px rgba(var(--sh),.5)}
#site .lang__menu[hidden]{display:none}
#site .lang__menu a{display:flex; align-items:baseline; justify-content:space-between; gap:1rem;
  text-decoration:none; font-size:.88rem; color:var(--dim); padding:.4rem .6rem; border-radius:6px}
#site .lang__menu a:hover{background:var(--band); color:var(--ink)}
#site .lang__menu a[aria-current="true"]{color:var(--ink); font-weight:600}
#site .lang__menu code{font-family:var(--display); font-size:.7rem; letter-spacing:.06em; opacity:.75}
@media (max-width:46rem){ #site .lang{display:none} }

#site .btn{display:inline-flex; align-items:center; gap:.5rem; background:var(--orange); color:var(--on-accent);
  text-decoration:none; font-weight:600; padding:.62rem 1.15rem; border-radius:999px; border:0; cursor:pointer}
#site .btn:hover{background:var(--rust)}
#site .btn--xl{font-size:1.05rem; padding:.9rem 1.8rem; box-shadow:0 14px 30px -14px var(--glow)}
#site .btn--ghost{background:transparent; color:var(--ink); border:1px solid var(--line)}
#site .btn--ghost:hover{background:var(--band)}
/* Sized off the label rather than in rem, so the glyph tracks whatever the
   button's font-size is - the compact header variant included. */
#site .btn svg{width:1em; height:1em; flex:none}
/* The header button is a compact variant of .btn. It never wraps: a two-line
   label in a 3.7rem bar is what a narrow header used to do to it, and the
   flex row would rather squeeze this than the nav beside it. */
#site .util .btn{font-size:.84rem; padding:.42rem .95rem; flex:none; white-space:nowrap}

/* — the narrow-screen menu — */
/* Hidden until the bar runs out of room for the row (64rem, measured above),
   which is also the width at which the wrapper stops being display:contents.
   One number, in one place: the moment anything would drop out of the bar,
   all of it is in here instead. */
#site .burger{display:none; margin-left:auto; align-items:center; justify-content:center;
  width:2.05rem; height:2.05rem; color:var(--dim); background:transparent;
  border:1px solid transparent; border-radius:7px; cursor:pointer}
#site .burger:hover{color:var(--ink); background:var(--band)}
#site .burger svg{width:1.15rem; height:1.15rem}
/* show the way out once you are in, the way the theme toggle does */
#site .burger .ex{display:none}
#site .top[data-nav="open"] .burger .bars{display:none}
#site .top[data-nav="open"] .burger .ex{display:block}

@media (max-width:64rem){
  #site .burger{display:flex}
  /* A panel hung off .top, which is sticky and therefore already the
     containing block - so it spans the bar edge to edge and carries .w's own
     inline padding rather than sitting inside it. No overflow:auto on it: the
     language menu below opens out of this box, and a scroll container would
     clip it. It is seven items and a row of icons; it fits. */
  #site .menu{display:none; position:absolute; left:0; right:0; top:100%;
    padding:.55rem clamp(1.1rem,4vw,2rem) .9rem; background:var(--card);
    border-bottom:1px solid var(--line);
    box-shadow:0 18px 34px -22px rgba(var(--sh),.5)}
  #site .top[data-nav="open"] .menu{display:block}
  /* the bar is translucent because the page scrolls under it; an open menu
     is a surface, so the two go solid together and read as one panel */
  #site .top[data-nav="open"]{background:var(--card)}
  #site .topnav{margin-left:0; display:block}
  #site .topnav a{display:block; font-size:1rem; padding:.6rem .7rem}
  /* In a column there is nothing to open a panel over: the caret goes to the
     far end and the list drops into the flow under it, ruled off rather than
     boxed, so a drawer of five links and two groups still reads as one list. */
  #site .nav__grp{display:block}
  #site .nav__top{width:100%; justify-content:space-between; font-size:1rem; padding:.6rem .7rem}
  #site .nav__sub{position:static; min-width:0; margin:0 0 .25rem .7rem;
    padding:0 0 0 .7rem; background:transparent; border:0; border-left:1px solid var(--line);
    border-radius:0; box-shadow:none}
  #site .nav__sub a{font-size:.95rem}
  #site .util{gap:.35rem; margin-top:.55rem; padding:.7rem .2rem 0;
    border-left:0; border-top:1px solid var(--line)}
  #site .util .lang{display:block; margin-left:.25rem}
  /* still downward: the drawer hangs from the top of the screen, so there is
     nothing above it to open into */
  #site .util .lang__menu{right:auto; left:0}
  /* the one control that is not an icon takes the rest of the row */
  #site .util .btn{margin-left:auto; font-size:.9rem; padding:.5rem 1.1rem}
}

/* — hero — */
#site .hero{padding-block:var(--secpad); margin: 20px;}
#site .hero .w{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);
  gap:clamp(2rem,5vw,4rem); align-items:center}
@media (max-width:60rem){ #site .hero .w{grid-template-columns:1fr} }
#site .hero h1{font-size:clamp(1.8rem,4vw,2.75rem); margin-bottom:1.1rem}
/* nowrap so "pure-text" never breaks across lines at its hyphen */
#site .hero h1 em{font-style:normal; color:var(--hl); white-space:nowrap}
#site .hero .lead{font-size:1.08rem; color:var(--dim); margin-bottom:1.9rem; max-width:40ch}
#site .cta{display:flex; gap:.7rem; align-items:center; flex-wrap:wrap; margin-bottom:1rem}
#site .plat{font-size:.85rem; color:var(--dim)}
#site .plat a{color:var(--link); text-decoration:none; font-weight:500}
/* A session rather than a screenshot. Its palette is fixed rather than
   themed: a terminal is dark in a light page and dark in a dark one. */
/* align-self overrides the row's align-items:center so the terminal fills the
   row instead of floating in the middle of it. The copy column to its left
   already sets that row's height, so this costs no layout at all — it hands
   the session the ~30px of dead gap that was sitting above and below it.
   The negative margin then lets it bleed a little way into the section's own
   padding: a third of it at most, so the hero keeps its breathing room and
   the section's height never moves (the copy column still sizes the row).
   Together that is about five more lines of world. Reset below the
   single-column breakpoint, where the padding is the only thing separating
   the terminal from the copy above it. */
/* `.heroterm__hold` is the stand-in the expand handler leaves in the grid
   while the panel is pinned over the page — see theme.js. It is listed with
   the panel everywhere the panel's own place in the row is described, so the
   two cannot drift: a stand-in that did not carry these margins would hold
   open a row a little taller than the one it replaced. */
#site .heroterm, #site .heroterm__hold{display:flex; align-self:stretch;
  margin-block:calc(-1 * clamp(.75rem, 2vw, 1.75rem))}
@media (max-width:60rem){ #site .heroterm, #site .heroterm__hold{margin-block:0} }
/* The terminal's palette, shared with the 404's room so the two are one
   terminal rather than two that agree today. */
#site .heroterm, #site .lostterm{--t-bg:#1e1813; --t-bar:#2b231b; --t-line:#3a2f24; --t-fg:#e8dcc6;
  --t-dim:#9c8f7c; --t-hot:#f56c27; --t-cool:#82c0c7; --t-sys:#8a9a5b}
#site .heroterm{flex-direction:column; min-height:23rem;
  background:var(--t-bg); border:1px solid var(--t-line); border-radius:10px; overflow:hidden;
  font-family:var(--display); font-size:.78rem; line-height:1.8; color:var(--t-fg);
  box-shadow:0 30px 60px -30px rgba(0,0,0,.7)}
/* The phone runs the real client too, and the 23rem above is a peephole there:
   it is a whole-screen layout folded into a third of one. Give the panel most
   of the screen instead - `svh`, not `vh`, so the box is the one that survives
   the URL bar sliding back down rather than the one that only exists while it
   is hidden. Still a min-height: the panel is free to be taller where there is
   room. Below rather than above the rule it overrides, because a media query
   adds no specificity — up there the 23rem simply won. */
@media (max-width:60rem){ #site .heroterm{min-height:min(34rem, 68svh)} }
#site .term__bar{display:flex; align-items:center; gap:.5ch; padding:.5rem .8rem;
  background:var(--t-bar); border-bottom:1px solid var(--t-line); font-size:.68rem; color:var(--t-dim)}
#site .term__dot{width:.5rem; height:.5rem; border-radius:50%; background:var(--t-line)}
#site .term__dot:first-child{background:#b83d00}
#site .term__name{margin-left:.7ch; letter-spacing:.02em}

/* Expand. Three hundred and forty pixels of console is enough to show what the
   demo is and not enough to sit and play in, so the title bar carries the one
   control that fixes that. */
#site .vh{position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0}
#site .term__grow{margin-left:auto; display:grid; place-items:center; width:1.4rem;
  height:1.4rem; padding:0; border:0; border-radius:4px; background:transparent;
  color:var(--t-dim); cursor:pointer; transition:color .12s, background .12s}
#site .term__grow svg{width:.85rem; height:.85rem}
#site .term__grow:hover{color:var(--t-fg); background:rgba(255,255,255,.06)}
#site .term__grow:focus-visible{outline:2px solid var(--t-hot); outline-offset:1px}

/* Expanded, the terminal is taken out of flow and pinned over the page. The
   grid row it leaves behind keeps its height — the copy column is what sizes
   that row — so the page underneath does not reflow while the scrim is up.
   Position, not re-parenting: moving a node that contains an iframe reloads
   the iframe, and reloading this one would throw away the session. */
/* Above the sticky header, which sits at 80. */
#site .heroterm.is-wide{position:fixed; inset:8vh 10vw; width:auto; height:auto;
  margin:0; z-index:201; box-shadow:0 40px 90px -30px rgba(0,0,0,.85)}
/* On a phone that inset is most of the screen. Expanded means expanded there:
   a thin frame so it still reads as a panel over the page, and `svh` again so
   the bottom of the console is not sitting behind the URL bar. */
@media (max-width:60rem){
  #site .heroterm.is-wide{inset:2svh 3vw}
}
/* Over everything, header included — the demo is the whole screen while it is
   open, and a nav bar floating above a modal reads as a bug. The blur is what
   makes it read as depth rather than as a dark rectangle; the flat colour
   underneath is the fallback where backdrop-filter is unsupported, so it is
   dark enough to stand on its own. */
#site .termscrim{position:fixed; inset:0; z-index:200; border:0; padding:0;
  background:rgba(12,9,7,.62);
  -webkit-backdrop-filter:blur(6px) saturate(.9); backdrop-filter:blur(6px) saturate(.9);
  animation:scrimin .22s ease-out}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  #site .termscrim{background:rgba(12,9,7,.82)}
}

/* The panel travels by transform, never by its box. Animating inset or
   width/height would relayout the iframe on every frame, and each relayout is
   a console re-wrap for the client inside it — thirty of them in a quarter
   second, for a shape nobody sees. The transform is set from script (see the
   expand handler): the panel is snapped to its final box, then transformed
   back onto the box it started in, then released. It grows out of the hero
   rather than appearing in the middle of the screen. */
@keyframes scrimin{from{opacity:0} to{opacity:1}}
#site .termscrim.is-shrinking{animation:scrimout .2s ease-in forwards}
@keyframes scrimout{from{opacity:1} to{opacity:0}}

@media (prefers-reduced-motion:reduce){
  #site .termscrim, #site .termscrim.is-shrinking{animation:none}
}

#site .term__stage{position:relative; flex:1; min-height:0}
#site .term__body{padding:1rem 1.15rem 1.25rem}
#site .term__body p{margin:0}
#site .ln--sys{color:var(--t-sys); font-size:.9em; letter-spacing:.04em; text-transform:uppercase}
#site .ln--room{color:var(--t-hot); font-weight:600}
#site .ln--desc{color:var(--t-dim)}
#site .ln--exits{color:var(--t-cool)}
#site .ln--gap{height:1.2em}
#site .ln--in{color:#f8f1df}
#site .ln--in::before{content:"> "; color:var(--t-hot); font-weight:600}
#site .caret{display:inline-block; width:.6ch; height:1em; background:var(--t-hot);
  translate:0 .18em; margin-left:.3ch; animation:blink 1.15s steps(1,end) infinite}
@keyframes blink{0%,50%{opacity:1}50.01%,100%{opacity:0}}
#site .heroterm .step{opacity:0}
#site .heroterm .step.is-on{opacity:1; animation:rise .3s ease-out both}
@keyframes rise{from{opacity:0; transform:translateY(.35em)} to{opacity:1; transform:none}}
@media (prefers-reduced-motion:reduce){
  #site .heroterm .step{opacity:1; animation:none}
  #site .caret{animation:none}
}

/* — the real thing: Mudlet Web, embedded — */
/* The scripted session above is the poster frame. The client is a real build
   of @mudlet/mudlet-web (demo/) in a same-origin iframe — same origin because
   it keeps its profile in IndexedDB, which Safari and Firefox deny to
   third-party frames. It boots after the page has loaded and only announces
   itself once it has printed, so a slow or blocked frame leaves the poster
   exactly as it is. */
/* The frame sits *under* the poster, at full opacity, from the moment it is
   created: it renders, boots and paints behind an opaque cover, and the swap
   is the cover fading off it. Fading the frame in over the poster instead
   left a beat where the poster was already hidden and the frame not yet
   opaque — a visible blink in the middle of the hero. */
#site .herolive{position:absolute; inset:0; z-index:0}
#site .herolive iframe{display:block; width:100%; height:100%; border:0; background:var(--t-bg)}

/* Everything the page draws itself is that cover: opaque, stage-filling, and
   above the frame until the client has printed. */
#site .term__body,#site .term__boot{position:absolute; inset:0; overflow:hidden;
  background:var(--t-bg)}
#site .term__body,#site .term__boot,#site .term__cmd{z-index:1}
/* A hard cut, not a fade. The client prints the same two lines the cover is
   showing, so lifting it changes nothing on screen — and then the client's
   own "*** connected ***" arrives underneath them, which is what connecting
   actually looks like. A cross-fade here read as a web page swapping an
   image; this reads as a client. */
#site .heroterm.is-live .term__body,
#site .heroterm.is-live .term__boot,
#site .heroterm.is-live .term__cmd{display:none}

/* — the boot state the live client fades out of — */
/* While the bundle is on the wire the frame shows what a client shows: its
   name and a connect line. The scripted session is not played at all in this
   state, so the visitor never reads a conversation that is about to be
   replaced by a different one — they watch one client connect once. Every
   metric here is the embedded console's: 16px/18px padding and the same
   type, so "connecting" sits exactly where the client's first line lands. */
#site .term__boot{display:none; padding:1rem 1.15rem 0}
#site .heroterm.is-booting .term__body{display:none}
#site .heroterm.is-booting .term__boot{display:block}
#site .heroterm.is-booting .term__cmd{display:flex}
/* Plain console type in the system colour — exactly what dfeedTriggers puts
   on screen from the demo package. No text-transform, tracking or size shift,
   or the cut would jump. */
#site .ln--boot{color:var(--t-sys)}
/* Dots on the same 300ms beat the demo package's timer uses, so the client
   picks the animation up where the page left it. A dot or two of phase
   difference at the handover is indistinguishable from the animation itself —
   a moving line is the one thing that can't show a seam. */
#site .dots i{font-style:normal; opacity:0; animation:dot 1.2s steps(1,end) infinite}
#site .dots i:nth-child(2){animation-delay:.3s}
#site .dots i:nth-child(3){animation-delay:.6s}
@keyframes dot{0%{opacity:0} 8%,100%{opacity:1}}
@media (prefers-reduced-motion:reduce){ #site .dots i{opacity:1; animation:none} }

/* A stand-in for the client's command bar, so the live frame doesn't arrive
   with a control the poster never had. Colours are the flattened values of
   the client's own tokens over the terminal ground. */
/* Measured off the real bar rather than eyeballed — every number here is
   one of Mudlet Web's own (.command-bar 6px/8px padding and 6px gap, a 26px
   field with 4px/8px/22px padding at 12px/16px, a 24px button at 11px), so
   the cut doesn't shift the control the visitor is about to type into. The
   prompt is a fixed 14px inline box, which is what puts the placeholder at
   the field's 22px text origin. */
#site .term__cmd{display:none; position:absolute; left:0; right:0; bottom:0;
  align-items:center; gap:6px; min-height:39px; padding:6px 8px;
  border-top:1px solid var(--t-line)}
#site .term__cmdbox{flex:1; height:26px; padding:4px 8px; font-size:12px; line-height:16px;
  border:1px solid var(--t-line); border-radius:6px; background:#221b15;
  color:var(--t-dim); white-space:nowrap; overflow:hidden}
#site .term__cmdbox::before{content:">"; display:inline-block; width:14px; color:var(--t-hot)}
/* system-ui, because that is what the client's buttons use — in the page's
   mono the same word measures 1.5px wider, and a wider button is a narrower
   field, which is a visible edge shifting at the cut. */
#site .term__cmdbtn{height:24px; padding:0 9px; line-height:22px; font-size:11px;
  font-family:system-ui,-apple-system,sans-serif;
  border:1px solid var(--t-line); border-radius:6px; color:var(--t-fg)}
@media (prefers-reduced-motion:reduce){ #site .dots i{opacity:1; animation:none} }

/* — the 404's room: the hero's terminal, with a real prompt — */
/* The same panel and the same line classes, in flow rather than over a frame:
   nothing boots here, so there is no cover and no stage. The output scrolls
   inside a fixed box so the prompt never moves while the visitor types at it. */
#site .lostterm{display:flex; flex-direction:column; max-width:46rem;
  background:var(--t-bg); border:1px solid var(--t-line); border-radius:10px; overflow:hidden;
  font-family:var(--display); font-size:.82rem; line-height:1.8; color:var(--t-fg);
  box-shadow:0 30px 60px -30px rgba(0,0,0,.7)}
#site .lostterm__out{height:clamp(14rem, 42svh, 22rem); overflow-y:auto;
  padding:1rem 1.15rem; overscroll-behavior:contain}
#site .lostterm__out p{margin:0}
#site .lostterm__out .ln--in{margin-top:.6em}
#site .lostterm__out a{color:var(--t-cool); text-underline-offset:.2em}
#site .lostterm__out a:hover{color:var(--t-fg)}
#site .lostterm__out [data-search]{color:var(--t-hot)}
#site .lostterm__out b{color:var(--t-fg); font-weight:600}
#site .lostterm__to{color:var(--t-dim)}
#site .lostterm__cmd{display:flex; align-items:center; gap:6px; padding:6px 8px;
  border-top:1px solid var(--t-line)}
#site .lostterm__mark{color:var(--t-hot); font-weight:600; padding-left:4px}
#site .lostterm__cmd input{flex:1; min-width:0; height:30px; padding:4px 8px;
  font:inherit; font-size:13px; color:var(--t-fg); background:#221b15;
  border:1px solid var(--t-line); border-radius:6px}
#site .lostterm__cmd input::placeholder{color:var(--t-dim)}
#site .lostterm__cmd input:focus-visible{outline:2px solid var(--t-hot); outline-offset:1px}
#site .lostterm__cmd button{height:30px; padding:0 12px; font:inherit; font-size:12px;
  color:var(--t-fg); background:transparent; border:1px solid var(--t-line); border-radius:6px;
  cursor:pointer}
#site .lostterm__cmd button:hover{background:rgba(255,255,255,.06)}
#site .lostterm__cmd button:focus-visible{outline:2px solid var(--t-hot); outline-offset:1px}

/* — section heading, with the > mark — */
#site .head{margin-bottom:2.25rem}
#site .head .eyebrow{display:flex; align-items:baseline; gap:.55ch;
  font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.78rem; letter-spacing:.02em;
  color:var(--dim); padding-bottom:.65rem; border-bottom:1px solid var(--line); margin-bottom:1.25rem}
#site .head .eyebrow::before{content:'>'; color:var(--mark); font-weight:600}
#site .head .eyebrow span{margin-left:auto; font-size:.72rem; letter-spacing:.08em; text-transform:uppercase}
/* the one eyebrow whose right-hand note is a link (the screenshots row) */
#site .head .eyebrow span a{color:var(--link); text-decoration:none}
#site .head .eyebrow span a:hover{text-decoration:underline}
#site .head h2{font-size:clamp(1.55rem,3.4vw,2.1rem); margin-bottom:.5rem}
#site .head .sub{color:var(--dim); max-width:52ch}
/* a head with no .sub under it would otherwise carry h2's bottom margin
   on top of .head's own, breaking the rhythm the other sections keep */
#site .head h2:last-child{margin-bottom:0}
/* and the same for a head that is nothing but its eyebrow, which is what the
   showcase is now - otherwise the eyebrow's rule and .head's own margin stack
   into half a screen of nothing before the cards */
#site .head .eyebrow:last-child{margin-bottom:0}
/* The screenshots row opens with a second .head, and .head carries no top
   margin - it is normally the first thing in a section. Butted straight up
   against the cards it read as their caption rather than as a new run. */
#site .showcase .cards + .head{margin-top:clamp(1.75rem,3.5vw,2.5rem)}

/* — signature: six cards, then three screenshots —
   This was a tab switcher that gave every claim the same 16:9 image slot, and
   only two of the six had a picture that filled it: "works anywhere" is a fact
   about operating systems and "free and open source" is a fact about who writes
   the client, and a screenshot of a session shows neither. So each card carries
   a small figure of its own, drawn in inc/front-art.php, and the real
   screenshots are the thumbnail row underneath - small enough that a crop stops
   mattering, which is the whole reason the old panel could not work. */
#site .showcase{background:var(--band); border-block:1px solid var(--line); padding-block:var(--secpad)}

#site .cards{display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem}
@media (max-width:62rem){ #site .cards{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:40rem){ #site .cards{grid-template-columns:1fr} }

#site .card{display:flex; flex-direction:column; gap:.9rem; padding:1.25rem;
  background:var(--card); border:1px solid var(--line); border-radius:12px}
#site .card h3{font-size:1.05rem; margin-bottom:.4rem}
#site .card p{color:var(--dim); font-size:.92rem; line-height:1.6; text-wrap:pretty}

/* One height for every figure, so the six titles sit on the same line whatever
   is drawn above them. */
#site .cardart{display:flex; align-items:center; gap:.85rem; height:2.75rem}

#site .cardart--platforms{gap:1.05rem}
#site .cardart__os{width:1.6rem; height:1.6rem; color:var(--ink)}

#site .cardart__bolt{width:1.9rem; height:1.9rem; color:var(--mark)}
#site .cardart__lua{width:2rem; height:2rem; color:var(--ink)}

/* Three windows offset behind each other, the front one in the accent. */
#site .cardart__stack{position:relative; width:4.2rem; height:2.5rem}
/* --dim rather than --line for the outline, and the two behind dimmed with
   opacity: band-on-card is nearly the same value in dark, which left the back
   two windows invisible there. */
#site .cardart__stack span{position:absolute; width:2.8rem; height:1.75rem;
  border:1.5px solid var(--dim); border-radius:4px; background:var(--card)}
#site .cardart__stack span:nth-child(1){opacity:.35}
#site .cardart__stack span:nth-child(2){left:.45rem; top:.35rem; opacity:.6}
#site .cardart__stack span:nth-child(3){left:.9rem; top:.7rem;
  border-color:var(--mark); background:var(--card)}

/* Filled rooms in --dim, not outlined in --line: --line on --card is nearly
   the same value in dark and the whole figure vanished there. --dim is the
   muted text colour, so it is guaranteed to carry against both cards. */
#site .cardart__map{width:4.25rem; height:2.75rem}
#site .cardart__map path{fill:none; stroke:var(--dim); stroke-width:2; stroke-linecap:round; opacity:.5}
#site .cardart__map rect{fill:var(--dim); opacity:.55}
#site .cardart__map rect.is-here{fill:var(--mark); opacity:1}

/* Overlapping faces; the last circle counts whoever did not fit. */
#site .cardart__faces{display:flex; align-items:center}
/* Two rings, doing two jobs: the 2px --card border is what separates one face
   from the one it overlaps, and the inset hairline is what gives an initials
   circle an edge at all - without it, band-on-card leaves the letters floating
   in dark. */
#site .cardart__face{display:flex; align-items:center; justify-content:center;
  width:2.05rem; height:2.05rem; border-radius:50%; overflow:hidden; background:var(--band);
  border:2px solid var(--card); box-shadow:inset 0 0 0 1px var(--line);
  font-family:var(--display); font-size:.62rem; color:var(--dim)}
#site .cardart__face + .cardart__face{margin-left:-.55rem}
#site .cardart__face img{width:100%; height:100%; object-fit:cover}
#site .cardart__face--more{color:var(--mark)}

#site .cardart__pill{display:inline-flex; align-items:center; gap:.4rem;
  padding:.2rem .6rem .2rem .5rem; background:var(--band); border:1px solid var(--line);
  border-radius:20px; font-family:var(--display); font-size:.78rem; text-decoration:none}
#site .cardart__pill svg{width:.9rem; height:.9rem; flex:none}
#site .cardart__pill:hover{border-color:var(--mark)}
#site .cardart__star{color:var(--mark)}
/* Green because green means "online" everywhere, not because the palette has a
   green - it deliberately does not, so this one is written out. */
#site .cardart__dot{width:.5rem; height:.5rem; border-radius:50%; background:#4f8a5f; flex:none}
/* the size of the room, then how many are in it right now */
#site .cardart__now{display:inline-flex; align-items:center; gap:.35rem; color:var(--dim)}
#site .cardart__now::before{content:"·"; color:var(--mark); opacity:.55; margin-right:.15rem}

/* — the screenshots, at thumbnail size —
   Three at random out of whatever /media/ holds. Cropping to 16:9 is fine here
   in a way it was not in the old full-width panel: at this size the crop reads
   as a thumbnail rather than as a picture with its top cut off. */
/* Set as a prompt, in the spec line's voice: the two of them bracket the
   screenshots and are the same kind of aside, so they read as one device
   rather than as a heading and a footnote. */

#site .shots{display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem}
@media (max-width:40rem){ #site .shots{grid-template-columns:1fr} }
#site .shot{display:block; overflow:hidden; background:var(--ink-d);
  border:1px solid var(--line); border-radius:10px}
#site .shot:hover{border-color:var(--mark)}
/* contain, not cover, and 16:10 rather than 16:9: the community shots run from
   0.86 to 1.86 wide, so no single ratio can crop-to-fill without taking the
   sides off half of them. Letterboxed against the dark ground they mostly are
   anyway, which is the same call the section made for its big panel. */
/* height:auto matters: wp_get_attachment_image() emits width/height attributes,
   and a specified height leaves aspect-ratio nothing to compute - the box kept
   the full height of a tall screenshot instead of the 16:10 asked for. */
#site .shot img{width:100%; height:auto; aspect-ratio:16/10; object-fit:contain; display:block}

/* the spec line: everything the six cards have no room to say. Set as a
   run rather than its own section, so the full list costs three lines
   instead of a screen, and reads as the spec sheet it is. */
#site .specs{margin-top:clamp(1.75rem,3.5vw,2.5rem); padding-top:clamp(1.4rem,2.5vw,1.8rem);
  border-top:1px solid var(--line); font-family:var(--display); font-size:.78rem;
  letter-spacing:.02em; line-height:1.9; color:var(--dim)}
#site .specs b{font-weight:600; color:var(--ink); margin-inline:.55ch 1ch}
#site .specs .sep{color:var(--mark); opacity:.55; margin-inline:.6ch}
#site .specs__more{opacity:.7}

/* — page routing: the prototype carries two views in one document — */
#site .page{display:none}
#site[data-page="home"] .page--home{display:block}
#site[data-page="dl"] .page--dl{display:block}
#site[data-page="news"] .page--news{display:block}
#site[data-page="post"] .page--post{display:block}

/* — download — */
#site .dl{padding-block:var(--secpad)}
/* this sub is a single 77-character line, not a paragraph; the 52ch measure
   inherited from .head broke it in two on screens with room to spare */
#site .dl .head .sub{max-width:none}
/* the whole point of the page: one button, already pointed at your OS */
#site .dlmain{background:var(--card); border:1px solid var(--line); border-radius:14px;
  padding:clamp(1.4rem,3.5vw,2.1rem); display:flex; gap:clamp(1.1rem,3vw,2rem);
  align-items:center; flex-wrap:wrap; box-shadow:0 18px 40px -28px rgba(var(--sh),.5)}
#site .dlmain[hidden]{display:none}   /* no build we can name: see theme.js */
#site .dlmain__icon{width:2.6rem; height:2.6rem; flex:none; color:var(--ink)}
#site .dlmain__icon svg{width:100%; height:100%}
#site .dlmain__os{flex:1; min-width:13rem}
#site .dlmain h3{font-size:clamp(1.15rem,2.6vw,1.45rem); margin-bottom:.35rem}
#site .dlmain .meta{font-family:var(--display); font-size:.8rem; color:var(--dim)}
#site .dlmain .alt{margin-top:.65rem; font-size:.86rem; color:var(--dim)}
#site .dlmain .alt[hidden]{display:none}
#site .dlmain .alt a{color:var(--link); font-weight:500; text-decoration:none}
#site .dlmain .alt a:hover{text-decoration:underline; text-underline-offset:3px}

/* The platform mark sits in a gutter of its own and everything else in one
   column beside it. Both measurements are named here because the wrapped
   layout has to put the size on that same column - see the query at the end
   of the block - and an indent typed out there would drift the day this icon
   changes size. */
#site .dltable{margin-top:clamp(1.75rem,3.5vw,2.5rem); border-top:1px solid var(--line);
  --dlmark:1.2rem; --dlgut:.8rem}
/* No row gap: the drawer below is a row of this grid and sits at zero height
   when it is shut, so a gap here would space every row out for a panel nobody
   has opened. What the wrapped layout needs instead is a margin, in the query
   at the end of the block. */
#site .dlrow{display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:0 1.5rem;
  align-items:center; padding:1rem .9rem; border-bottom:1px solid var(--line)}
/* the row matching the visitor gets marked, so the table agrees with the button */
#site .dlrow[data-here="true"]{background:var(--band)}
#site .dlrow__id{display:flex; gap:var(--dlgut); align-items:flex-start}
#site .dlrow__id > svg{width:var(--dlmark); height:var(--dlmark); flex:none; margin-top:.2rem;
  color:var(--ink)}
#site .dlrow b{font-family:var(--display); font-weight:600; font-size:.98rem}
#site .dlrow .note{color:var(--dim); font-size:.86rem; margin-left:.7ch}
/* the checksum is shortened for the eye but carries the full 64 characters,
   so clicking copies something usable rather than the truncation */
#site .sha{display:flex; width:fit-content; align-items:center; gap:.55ch; margin:.5rem 0 0 -.4rem;
  padding:.15rem .4rem; font-family:var(--display); font-size:.71rem; letter-spacing:.015em;
  color:var(--dim); background:transparent; border:1px solid transparent; border-radius:5px;
  cursor:pointer; transition:.15s; max-width:100%}
#site .sha:hover{color:var(--ink); background:var(--band); border-color:var(--line)}
#site .sha svg{width:.85rem; height:.85rem; flex:none; opacity:.55; transition:opacity .15s}
#site .sha:hover svg{opacity:1}
#site .sha__v{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
#site .sha[data-copied]{color:var(--mark); border-color:var(--mark); background:transparent}
/* when the clipboard is unavailable the value is revealed to select by hand */
#site .sha[data-revealed] .sha__v{white-space:normal; word-break:break-all}
#site .sha[data-revealed]{cursor:text; user-select:text}
#site .dlrow .size{font-family:var(--display); font-size:.82rem; color:var(--dim); white-space:nowrap}
#site .dlrow .btn{font-size:.84rem; padding:.42rem .95rem}
/* ── the hand-off, one row at a time ──────────────────────────────────
   The build is not chosen twice. A row already names a platform, carries
   its size and its checksum and links its installer, so the way to ask
   for that build's code is a button in that row - not a select under the
   table restating four rows the visitor is looking at.

   Each row keeps a drawer, shut at zero height, and the icons above it
   choose which face it shows. Rows are independent: two open drawers is
   a visitor comparing two builds, not a mistake to correct. */
#site .dlrow__acts{display:flex; align-items:center; gap:.45rem}
/* Square, quiet, and the same height as the ghost button beside them, so
   the three read as one control group rather than as decoration that
   drifted into the row. */
#site .dlact{display:grid; place-items:center; width:2rem; height:2rem; flex:none;
  color:var(--dim); background:transparent; border:1px solid var(--line);
  border-radius:8px; cursor:pointer; transition:.15s}
#site .dlact[hidden]{display:none}   /* no script, no drawer to open */
/* The way round the mirror: the same file, from GitHub. The mark alone, and
   deliberately without the .dlact border - it navigates away rather than
   opening the drawer, so it should not read as a fourth control in the group.
   Quiet on purpose: this is the answer to "that link won't load for me", not a
   second thing to choose between. */
#site .dlsrc{display:grid; place-items:center; width:1.6rem; height:2rem; flex:none;
  color:var(--dim); transition:.15s}
#site .dlsrc svg{width:1.05rem; height:1.05rem}
#site .dlsrc:hover{color:var(--ink)}
#site .dlact svg{width:1.05rem; height:1.05rem}
#site .dlact:hover{color:var(--ink); background:var(--band)}
/* The open one stays lit, because its drawer is the only thing on screen
   saying which button opened it. */
#site .dlact[aria-expanded="true"]{color:var(--on-accent); background:var(--orange);
  border-color:var(--orange)}

/* The drawer spans the row it belongs to, and slides on a grid track going
   0fr -> 1fr. That animates to the pane's own height with nobody measuring
   anything: no max-height to guess at, and none to go stale when the pane
   grows a line it did not have - which the mail form does, every time it
   reports back. The two faces are not the same height either, so swapping
   between them animates for free.

   The inner box is what clips; it also goes invisible when shut, which is
   what keeps a closed drawer's button and field out of the tab order. */
#site .dlmore{grid-column:1/-1; display:grid; grid-template-rows:0fr;
  transition:grid-template-rows .28s cubic-bezier(.4,0,.2,1)}
#site .dlmore[data-open="true"]{grid-template-rows:1fr}
/* The side padding is room for a focus ring, and the negative margin puts
   the content back where it was: what clips has to be the box the track
   sizes, so anything drawn outside a control at the drawer's left edge -
   the outline on the mail form's field, which starts there - is cut off
   without it. Vertically the clip stays tight, which is the whole point. */
#site .dlmore__in{overflow:hidden; padding:0 4px; margin:0 -4px;
  visibility:hidden; transition:visibility 0s .28s}
#site .dlmore[data-open="true"] .dlmore__in{visibility:visible; transition:visibility 0s}
@media (prefers-reduced-motion:reduce){
  #site .dlmore{transition:none}
  #site .dlmore__in{transition:none}
}
/* One face at a time: the icons swap what the open drawer is showing
   rather than each opening a drawer of its own. */
#site .dlpane{display:none; padding:1.1rem 0 .35rem}
#site .dlmore[data-face="qr"] .dlpane--qr{display:grid}
#site .dlpane p{color:var(--dim); font-size:.88rem; max-width:52ch; margin:0}
#site .dlpane__acts{display:flex; flex-wrap:wrap; align-items:center; gap:.6rem; margin-top:.8rem}
#site .dlpane__acts .btn{font-size:.84rem; padding:.42rem .95rem}

/* The code is drawn dark on white whatever the theme is doing: a QR in
   theme colours is one a phone has to work at, and the panel can afford
   a white card in the middle of it. */
#site .dlpane--qr{grid-template-columns:auto minmax(0,1fr); gap:1.15rem; align-items:center}
#site .dlqr{width:7.4rem; height:7.4rem; padding:.4rem; border-radius:10px;
  background:#fff; border:1px solid var(--line)}
#site .dlqr svg{display:block; width:100%; height:100%}
/* The link in full, because a code nobody can scan is still a link
   somebody can read. It wraps rather than scrolls: this is a URL, and
   the halves of a wrapped one are still usable. */
#site .dlurl{font-family:var(--display); font-size:.78rem; color:var(--dim);
  word-break:break-all}

/* Narrow, the three controls no longer fit beside a platform name: the name
   takes the width to itself, and what you can do with the build - its size,
   the two hand-off buttons, the download - lines up on a row underneath.
   The size is indented past the platform mark so that it, the name and the
   checksum share one left edge; left where the grid puts it, it would sit
   under the mark instead and give the row two ragged columns. */
@media (max-width:40rem){
  #site .dlrow{grid-template-columns:minmax(0,1fr) auto}
  #site .dlrow__id{grid-column:1/-1; grid-row:1}
  #site .dlrow .size{grid-column:1; grid-row:2; align-self:center; margin-top:.7rem;
    margin-left:calc(var(--dlmark) + var(--dlgut))}
  #site .dlrow__acts{grid-column:2; grid-row:2; margin-top:.7rem}
  #site .dlmore{grid-row:3}
  #site .dlpane--qr{grid-template-columns:1fr; justify-items:center; text-align:center}
  #site .dlpane p{max-width:none}
  #site .dlpane__acts{justify-content:center}
}

/* 17rem lands three per row, so five cards break 3+2 rather than
   stranding the fifth alone on a second row */
#site .ways{display:grid; gap:.85rem; grid-template-columns:repeat(auto-fit,minmax(17rem,1fr))}
#site .way{background:var(--card); border:1px solid var(--line); border-radius:12px;
  padding:1.15rem 1.25rem 1.25rem}
#site .way > svg{width:1.3rem; height:1.3rem; color:var(--mark); margin-bottom:.7rem}
#site .way h3{font-size:.98rem; margin-bottom:.4rem}
#site .way p{font-size:.88rem; line-height:1.65; color:var(--dim)}
#site .way a{color:var(--link); font-weight:500; text-decoration:none}
#site .way a:hover{text-decoration:underline; text-underline-offset:3px}
/* No .way--wide any more: "Build it yourself" was the only tile that ever took
   two columns, and it did so to keep its `git clone` line on one row. It does
   not hold that at one column, and the note that used to claim it did had the
   wrong number for the narrowest tile: three columns bottom out at 278px, not
   at 320px. Measured, the line needs 305px and gets it only at the widest
   three-column tile (364px) and in the two-column layouts under 850px —
   through the middle of the three-column band it scrolls, which is what the
   rule below has always been for. The tile is one column because six tiles
   make two rows of three; the clone line scrolls, as it was always going to. */
/* It wraps rather than scrolls, which is the opposite of what it used to do.
   A command that wraps is a command somebody can mistype at the break, so the
   old rule kept it on one line and let the tile scroll sideways — and since
   the line only ever fitted at the widest tile (see above), what that bought
   in practice was a scrollbar inside a card at most window widths. The copy
   button beside it is what settles the argument: the break cannot be mistyped
   by anyone who presses the button, and the visitor no longer has to drag a
   309px line through a 208px window to read the end of it. `anywhere` rather
   than break-all so it takes the space in "git clone" first and only breaks
   inside the URL when even that is not enough. */
#site .way code{display:block; margin-top:.6rem; padding:.5rem .6rem; background:var(--band);
  overflow-wrap:anywhere;
  border:1px solid var(--line); border-radius:6px; font-family:var(--display);
  font-size:.72rem; color:var(--ink)}
/* The clone line and the button that copies it are one box, not two beside
   each other. A second bordered box with its own gap costs 38px of a tile
   that has 322px at its widest and 238px at its narrowest, and since the line
   wraps, every pixel it takes is a pixel closer to a third line. Sharing one
   box costs 24px instead, and reads as one control rather than as a button
   that drifted up against a field. So the border, the background and the
   radius move up here from the code, and the button keeps only its ink.
   Centred rather than aligned to the first line, because what it copies is
   all of the lines. */
#site .clone{display:flex; align-items:center; margin-top:.6rem;
  background:var(--band); border:1px solid var(--line); border-radius:6px}
#site .clone code{margin-top:0; flex:1 1 auto; min-width:0;
  background:transparent; border:0; border-radius:0}
#site .clone__cp{flex:none; display:grid; place-items:center; width:1.6rem; height:1.6rem;
  margin-right:.15rem; color:var(--dim); background:transparent; border:0;
  border-radius:4px; cursor:pointer; transition:.15s}
#site .clone__cp[hidden]{display:none}   /* no script, nothing to copy with */
#site .clone__cp:hover{color:var(--ink)}
#site .clone__cp svg{width:.9rem; height:.9rem}
/* the tick takes the copy icon's place for a second and a half, the way the
   theme toggle and the burger show the state they would move you to */
#site .clone__cp .ok{display:none}
#site .clone__cp[data-copied]{color:var(--mark)}
#site .clone__cp[data-copied] .cp{display:none}
#site .clone__cp[data-copied] .ok{display:block}
/* A file name inside a sentence is not a command to run: the rule above dresses
   the clone line, and this undoes the block half of it for the one word the
   Portable tile sets in the middle of its prose. */
#site .way p code{display:inline; margin:0; padding:.08rem .35rem; border:0;
  border-radius:4px; white-space:normal; word-break:normal; font-size:.85em}

/* said once, not once per platform tab as the current page does */
#site .calls{display:grid; gap:.85rem; grid-template-columns:repeat(auto-fit,minmax(19rem,1fr))}
#site .call{background:var(--band); border:1px solid var(--line); border-left:3px solid var(--mark);
  border-radius:0 12px 12px 0; padding:1.15rem 1.3rem}
#site .call h3{font-size:.98rem; margin-bottom:.35rem; display:flex; align-items:center; gap:.5rem}
#site .call h3 svg{width:1.15rem; height:1.15rem; color:var(--mark); flex:none}
#site .call p{font-size:.88rem; line-height:1.65; color:var(--dim)}
#site .call a{color:var(--link); font-weight:500; text-decoration:none}
#site .call a:hover{text-decoration:underline; text-underline-offset:3px}

#site .fineprint{margin-top:clamp(1.75rem,3.5vw,2.5rem); padding-top:1.4rem;
  border-top:1px solid var(--line); font-size:.79rem; line-height:1.75;
  color:var(--dim); max-width:66ch}

/* — games: modelled on Mudlet's own profile picker — */
#site .games{padding-block:var(--secpad)}
#site .pgrid{display:grid; gap:.7rem; grid-template-columns:repeat(auto-fill,minmax(10.5rem,1fr))}
#site .pcard{background:var(--card); border:1px solid var(--line); border-radius:11px; padding:.85rem;
  display:flex; flex-direction:column; gap:.6rem; text-decoration:none; transition:.15s}
#site .pcard:hover{border-color:var(--accent2); box-shadow:0 8px 20px -12px rgba(var(--sh),.45)}
#site .plogo{height:3.1rem; display:grid; place-items:center; background:var(--cream);
  border-radius:7px; overflow:hidden; padding:.35rem}
/* min-*:0 releases the grid item's automatic minimum size, which for an image
   is its intrinsic pixels and outranks max-height. These tiles are narrow
   enough that width binds first, so nothing overflows today — but a wider tile
   with the same rule does, which is exactly what happened to .rgame__logo in
   the theme's blocks.css. max-* only shrinks, so small logos still draw at
   their own size rather than being upscaled to fill the tile. */
#site .plogo img{min-width:0; min-height:0; max-height:100%; max-width:100%;
  width:auto; height:auto; object-fit:contain}
#site .pcard b{font-size:.88rem; font-weight:600; line-height:1.3}
#site .pcard span{font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.72rem; color:var(--dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
#site .pmore{background:var(--band); border-style:dashed; align-items:center; justify-content:center;
  text-align:center; gap:.2rem}
#site .pmore b{font-size:1.15rem; font-family:var(--display); color:var(--link)}
#site .pmore span{font-family:var(--body); white-space:normal}

/* — prose — */
#site .sec{padding-block:var(--secpad)}
#site .split{display:grid; grid-template-columns:repeat(auto-fit,minmax(19rem,1fr)); gap:2.75rem}
#site .split h2{font-size:clamp(1.4rem,3vw,1.85rem); margin-bottom:1rem}
#site .split p{color:var(--dim); margin-bottom:1rem}
/* The line under the quotation. Was an inline style until the copy moved into
   inc/front-content.php and the template stopped hand-writing the markup. */
#site .split__note{margin-top:1rem}
#site .quote{background:var(--band); border-left:3px solid var(--accent2); border-radius:0 10px 10px 0;
  padding:1.3rem 1.4rem; font-size:.97rem; line-height:1.65; color:var(--ink)}
#site .quote cite{display:block; margin-top:.6rem; font-style:normal;
  font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.75rem; letter-spacing:.05em;
  text-transform:uppercase; color:var(--dim)}

/* — news — */
#site .newsband{background:var(--band); border-block:1px solid var(--line); padding-block:var(--secpad)}
#site .ncards{display:grid; grid-template-columns:repeat(auto-fit,minmax(17rem,1fr)); gap:1.1rem}
#site .ncard{background:var(--card); border:1px solid var(--line); border-radius:12px; padding:1.3rem 1.35rem;
  text-decoration:none; display:flex; flex-direction:column; gap:.5rem; transition:.15s}
#site .ncard:hover{transform:translateY(-2px); box-shadow:0 14px 30px -18px rgba(var(--sh),.5)}
#site .ncard .tag{font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.7rem;
  letter-spacing:.09em; text-transform:uppercase; color:var(--link); font-weight:600}
#site .ncard h3{font-size:1.05rem}
#site .ncard p{font-size:.9rem; color:var(--dim); line-height:1.55}
#site .ncard time{font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.76rem;
  color:var(--dim); margin-top:auto}

/* — hop in: the v1 closing section, kept. Dark, so it reads as one
     outro zone with the footer beneath it. Orange lifts to --ember
     here because #c44400 is too dark to sit on the terminal ground. — */
#site .hopin{background:var(--ink-d); color:var(--on-d);
  padding-block:var(--secpad); border-bottom:1px solid var(--line-d)}
#site .hopin .eyebrow{color:var(--on-d-dim); border-bottom-color:var(--line-d)}
#site .hopin .eyebrow::before{color:var(--ember)}
#site .hopin h2{color:var(--on-d)}
#site .hopin .sub{color:var(--on-d-dim)}
#site .hops{display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(15.5rem,1fr))}
#site .hop{border:1px solid var(--line-d); border-radius:12px; padding:1.4rem 1.35rem 1.5rem;
  background:var(--ink-d2); display:flex; flex-direction:column; gap:.65rem; align-items:flex-start}
#site .hop > svg{width:1.4rem; height:1.4rem; color:var(--ember)}
#site .hop h3{font-size:1.02rem; color:var(--on-d)}
#site .hop p{font-size:.93rem; line-height:1.65; color:var(--on-d-dim); flex:1}
#site .hop__go{font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.85rem; font-weight:500;
  text-decoration:none; color:var(--ember); display:inline-flex; gap:.55ch; align-items:baseline}
#site .hop__go::before{content:'>'; color:var(--ember); opacity:.7}
#site .hop__go:hover{text-decoration:underline; text-underline-offset:3px}

/* — footer: the v1 terminal footer, kept — */
#site .foot{background:var(--ink-d); color:var(--on-d-dim);
  font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.82rem; line-height:1.7}
#site .foot .w{padding-block:3rem 0}
#site .foot__grid{display:flex; gap:2.5rem 3.25rem; flex-wrap:wrap; margin-bottom:2.5rem}
#site .foot__col{display:flex; flex-direction:column; gap:.45rem}
#site .foot__col b{color:var(--on-d); font-weight:500; font-size:.72rem;
  letter-spacing:.11em; text-transform:uppercase; margin-bottom:.35rem}
#site .foot__col a{text-decoration:none; display:flex; align-items:center; gap:.55rem; width:fit-content}
#site .foot__col a:hover{color:var(--ember)}
#site .foot__col svg{width:1rem; height:1rem; flex:none; opacity:.75}
#site .foot__col a:hover svg{opacity:1}
#site .foot__brand{display:flex; align-items:center; gap:.5rem; color:var(--on-d);
  font-size:1rem; font-weight:600; margin-bottom:.85rem}
#site .foot__brand img{width:1.5rem; height:1.5rem}
#site .foot__blurb{max-width:26ch; font-size:.79rem}
#site .foot__end{display:flex; gap:1.2rem; flex-wrap:wrap; align-items:center;
  border-top:1px solid var(--line-d); padding-block:1.4rem 3rem; font-size:.76rem}
#site .foot__end a{text-decoration:none}
#site .foot__end a:hover{color:var(--ember)}
#site .langs{margin-left:auto; display:flex; gap:.8rem}
#site .langs a[aria-current]{color:var(--ember)}

@media (prefers-reduced-motion:reduce){ #site .ncard:hover{transform:none} }

/* the nav gains a current state now that a nav item leads somewhere */
#site .topnav a[aria-current]{color:var(--ink); background:var(--band)}

/* ── archive ──────────────────────────────────────────────────────── */
#site .archive{padding-block:var(--secpad)}

/* The newest post gets the room the list cannot give it — on a release day
   it is the one thing anyone came for. Its right rail is the release itself
   rather than a second helping of prose: the numbers off the changelog, set
   in the console face, so the card says "4.22.0 shipped" before it is read. */
#site .feat{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,14rem);
  gap:clamp(1.5rem,4vw,2.75rem); align-items:center; text-decoration:none;
  background:var(--card); border:1px solid var(--line); border-radius:14px;
  padding:clamp(1.4rem,3.5vw,2.1rem); transition:border-color .15s, box-shadow .15s;
  box-shadow:0 18px 40px -28px rgba(var(--sh),.5)}
#site .feat:hover{border-color:var(--accent2); box-shadow:0 22px 46px -26px rgba(var(--sh),.55)}
@media (max-width:52rem){ #site .feat{grid-template-columns:1fr} }
#site .feat[hidden]{display:none}
#site .feat__lead{min-width:0}
#site .feat h3{font-size:clamp(1.3rem,3vw,1.75rem); margin:.6rem 0 .7rem; transition:color .15s}
#site .feat:hover h3{color:var(--link)}
#site .feat p{color:var(--dim); font-size:.98rem; line-height:1.65; max-width:52ch}
#site .feat__m{display:flex; align-items:center; gap:.75ch; flex-wrap:wrap; margin-top:1rem;
  font-family:var(--display); font-size:.78rem; color:var(--dim)}
#site .feat__m .who{color:var(--ink); font-weight:600}
#site .feat__m .dot{opacity:.4}
/* the release, as a spec block */
#site .relbox{border:1px solid var(--line); border-radius:11px; background:var(--band);
  padding:1rem 1.1rem; font-family:var(--display)}
#site .relbox b{display:block; font-size:1.5rem; font-weight:600; letter-spacing:-.03em; color:var(--link)}
#site .relbox .when{font-size:.72rem; letter-spacing:.06em; text-transform:uppercase; color:var(--dim)}
#site .relbox dl{display:grid; grid-template-columns:auto 1fr; gap:.15rem .8ch; margin:.85rem 0 0;
  padding-top:.8rem; border-top:1px solid var(--line); font-size:.78rem}
#site .relbox dt{color:var(--ink); font-weight:600; text-align:right}
#site .relbox dd{margin:0; color:var(--dim)}

/* ── the archive grid ──────────────────────────────────────────────
   List and rail, with the featured post spanning full width above them.
   The rail area spans both rows so the sticky sidebar has a containing
   block to travel down — the standard grid recipe, and the reason the
   pager is a grid item rather than a sibling below the grid. */
#site .arch{display:grid; grid-template-columns:minmax(0,1fr) 15.5rem;
  grid-template-areas:"list rail" "pager rail";
  column-gap:clamp(1.75rem,3.5vw,3rem); align-items:start;
  margin-top:clamp(2rem,4vw,2.75rem)}
#site .plist{grid-area:list}
#site .pager{grid-area:pager}
#site .rail{grid-area:rail; position:sticky; align-self:start;
  top:4.6rem;
  display:flex; flex-direction:column; gap:1.6rem}
/* The chip row is the narrow-screen half of the category filter, so it is off
   by default and switched on in the query below — declared here, ahead of that
   query, because a later same-specificity display:none would win over it. */
#site .filters{display:none; align-items:center; gap:.4rem; flex-wrap:wrap;
  padding-bottom:1.1rem; border-bottom:1px solid var(--line)}

/* Below the two-column threshold the rail drops under the list, so the
   category filter moves back into the chip row above it — one control,
   never two on screen at once, and both driven by the same handler. */
@media (max-width:68rem){
  #site .arch{grid-template-columns:1fr; grid-template-areas:"filters" "list" "pager" "rail";
    row-gap:1.75rem}
  #site .filters{display:flex; grid-area:filters}
  #site .rail__cats{display:none}
  /* Stacked, the remaining panels land a column of links directly on top of
     the footer's column of links. Laid across instead, the rail reads as a
     closing strip and stays a third of the height. */
  #site .rail{position:static; margin-top:1.25rem; padding-top:1.9rem;
    border-top:1px solid var(--line); display:grid; align-items:start;
    grid-template-columns:repeat(auto-fit,minmax(12rem,1fr)); gap:1.6rem 2.25rem}
}

/* ── the rail ──────────────────────────────────────────────────────
   The original page carried Get Help, Recent Posts and Categories here.
   Recent Posts is gone: its three entries are the first three rows of
   the list beside it. What replaces it is the thing 178 posts across
   eighteen years actually need — a way to jump to a year. */
#site .rpanel > b{display:block; font-family:var(--display); font-size:.72rem;
  font-weight:500; letter-spacing:.11em; text-transform:uppercase; color:var(--dim);
  padding-bottom:.55rem; margin-bottom:.7rem; border-bottom:1px solid var(--line)}
#site .catlist{display:flex; flex-direction:column; gap:.05rem; margin-inline:-.5rem}
#site .cat{display:flex; align-items:baseline; gap:.7ch; width:100%; text-align:left;
  font-family:var(--body); font-size:.9rem; color:var(--dim); background:transparent;
  border:0; padding:.38rem .5rem; border-radius:7px; cursor:pointer; transition:.13s}
/* the same dot the list's tags carry, so the rail and the rows read as one system */
#site .cat::before{content:''; width:.42rem; height:.42rem; border-radius:50%; flex:none;
  background:currentColor; opacity:.4; translate:0 -.15em}
#site .cat[data-cat="release"]::before{background:var(--mark); opacity:1}
#site .cat[data-cat="informational"]::before{background:var(--teal); opacity:1}
#site .cat[data-cat="press"]::before{background:#8a9a5b; opacity:1}
#site .cat .n{margin-left:auto; font-family:var(--display); font-size:.72rem; opacity:.65}
#site .cat:hover{background:var(--band); color:var(--ink)}
#site .cat[aria-pressed="true"]{background:var(--band); color:var(--link); font-weight:600}

/* The year jump was the one control here the browser drew rather than this
   stylesheet: a native <select>, with an OS dropdown behind it and an OS arrow
   on it, in the middle of a rail of hand-drawn rows. It is a <details> now, and
   what opens is the years as what they already were — links to real archives,
   in the same shape as the categories above them, count and all. It works with
   no JavaScript, which the select could not: a select cannot be a link, so it
   needed a change handler to go anywhere. */
#site .ydrop{border:1px solid var(--line); border-radius:8px; background:var(--card)}
#site .ydrop:hover{border-color:var(--accent2)}
#site .ydrop summary{display:flex; align-items:center; gap:.5rem; padding:.5rem .6rem;
  font-family:var(--display); font-size:.84rem; color:var(--ink); cursor:pointer;
  border-radius:7px; list-style:none}
/* Safari still draws the disclosure triangle from the shadow DOM. */
#site .ydrop summary::-webkit-details-marker{display:none}
#site .ydrop summary .crt{width:.7rem; height:.7rem; margin-left:auto; opacity:.7;
  transition:rotate .15s}
#site .ydrop[open] summary .crt{rotate:180deg}
/* Two columns: nineteen years down one of them is a scroll in a panel that is
   trying to save somebody a scroll. Two columns is also what sets the cap —
   16rem clears the ten rows this site has, so nothing scrolls today, and still
   stops the sticky rail growing past the window on the site that has been
   posting for thirty years. The bar is styled for that day. */
#site .ylist{display:grid; grid-template-columns:1fr 1fr; gap:.05rem;
  max-height:16rem; overflow-y:auto; padding:.3rem; border-top:1px solid var(--line);
  scrollbar-width:thin; scrollbar-color:var(--dim) transparent}
#site .ylist a{display:flex; align-items:baseline; gap:.6ch; padding:.32rem .45rem;
  border-radius:6px; font-family:var(--display); font-size:.8rem; color:var(--dim);
  text-decoration:none; transition:.13s}
#site .ylist a .n{margin-left:auto; font-size:.72rem; opacity:.6}
#site .ylist a:hover{background:var(--band); color:var(--ink)}
#site .ylist a[aria-current="true"]{background:var(--band); color:var(--link); font-weight:600}

#site .rlinks{display:flex; flex-direction:column; gap:.55rem; font-size:.89rem}
#site .rlinks a{display:flex; gap:.6ch; color:var(--dim); text-decoration:none}
#site .rlinks a::before{content:'>'; font-family:var(--display); color:var(--mark); opacity:.5}
#site .rlinks a:hover{color:var(--link)}
#site .rlinks a:hover::before{opacity:1}

/* ── the chips themselves (see #site .filters above) ─────────────────── */
#site .chip{display:inline-flex; align-items:baseline; gap:.6ch; font-family:var(--display);
  font-size:.8rem; font-weight:500; color:var(--dim); background:transparent;
  border:1px solid var(--line); border-radius:999px; padding:.34rem .85rem; cursor:pointer;
  transition:.15s}
#site .chip b{font-weight:600; font-size:.72rem; opacity:.65}
#site .chip:hover{color:var(--ink); background:var(--band)}
#site .chip[aria-pressed="true"]{background:var(--orange); border-color:var(--orange); color:var(--on-accent)}
#site .chip[aria-pressed="true"] b{opacity:.75}
/* ── the list ──────────────────────────────────────────────────────
   Rows on a date rail, not a card grid. The homepage already spends a
   grid of cards on three posts; an archive of 178 wants a log, and the
   date is what people scan it by. */
#site .pyear{display:flex; align-items:center; gap:1.1ch; margin:2.25rem 0 .25rem;
  font-family:var(--display); font-size:.75rem; font-weight:600; letter-spacing:.14em; color:var(--dim)}
#site .pyear::before{content:'>'; color:var(--mark)}
#site .pyear::after{content:''; flex:1; height:1px; background:var(--line)}
#site .pyear[hidden]{display:none}
/* the rail's year jump scrolls to these, and the header is sticky over them */
#site .pyear{scroll-margin-top:5.25rem}
/* the first heading sets the top of the list, which has to line up with the
   top of the rail beside it */
#site .plist > .pyear:first-child{margin-top:0}

#site .prow{display:grid; grid-template-columns:4.75rem minmax(0,1fr) auto; gap:.35rem 1.4rem;
  align-items:baseline; padding:1.1rem .85rem; border-bottom:1px solid var(--line);
  text-decoration:none; transition:background .15s}
#site .prow:hover{background:var(--band)}
#site .prow[hidden]{display:none}
#site .prow__d{grid-row:1 / span 3; font-family:var(--display); font-size:.8rem; color:var(--dim);
  padding-top:.1rem}
#site .prow h3{font-size:1.02rem; transition:color .15s}
/* the mark rides in from the rail on hover — the same > the eyebrows use */
#site .prow h3::before{content:'>'; color:var(--mark); opacity:0; margin-left:-1.1ch;
  margin-right:.35ch; transition:opacity .15s}
#site .prow:hover h3{color:var(--link)}
#site .prow:hover h3::before{opacity:1}
#site .prow p{grid-column:2; font-size:.89rem; line-height:1.6; color:var(--dim);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden}
#site .prow__m{grid-column:2; margin-top:.3rem; font-family:var(--display); font-size:.75rem; color:var(--dim)}
#site .prow__m .dot{opacity:.4; margin-inline:.6ch}
#site .prow .tagpill{grid-column:3; grid-row:1; justify-self:end}
@media (max-width:46rem){
  #site .prow{grid-template-columns:1fr auto; gap:.3rem .9rem}
  #site .prow__d{grid-row:1; grid-column:1; font-size:.74rem}
  #site .prow h3,#site .prow p,#site .prow__m{grid-column:1 / -1}
  #site .prow h3::before{display:none}
  #site .prow .tagpill{grid-row:1; grid-column:2}
}

/* Category, as a dot and a word. Colour carries the category but never the
   meaning — the label is always there, and the dot is decorative, which is
   what lets it use the teal and the olive at their real values. */
#site .tagpill{display:inline-flex; align-items:center; gap:.6ch; font-family:var(--display);
  font-size:.7rem; letter-spacing:.09em; text-transform:uppercase; color:var(--dim); white-space:nowrap}
#site .tagpill::before{content:''; width:.42rem; height:.42rem; border-radius:50%;
  background:currentColor; opacity:.5; flex:none}
#site .tagpill[data-cat="release"]::before{background:var(--mark); opacity:1}
#site .tagpill[data-cat="informational"]::before{background:var(--teal); opacity:1}
#site .tagpill[data-cat="press"]::before{background:#8a9a5b; opacity:1}

#site .pempty{padding:3rem .85rem; text-align:center; color:var(--dim); font-size:.92rem}
#site .pempty[hidden]{display:none}

/* ── pagination ───────────────────────────────────────────────────── */
#site .pager{display:flex; align-items:center; gap:.3rem; flex-wrap:wrap; margin-top:2.25rem}
#site .pager a,#site .pager .gap{display:grid; place-items:center; min-width:2.15rem; height:2.15rem;
  padding-inline:.55rem; font-family:var(--display); font-size:.85rem; color:var(--dim);
  text-decoration:none; border-radius:8px}
#site .pager a:hover{background:var(--band); color:var(--ink)}
#site .pager a[aria-current]{background:var(--orange); color:var(--on-accent); font-weight:600}
#site .pager a[aria-disabled="true"]{opacity:.35; pointer-events:none}
#site .pager .count{margin-left:auto; font-family:var(--display); font-size:.78rem; color:var(--dim)}
@media (max-width:46rem){ #site .pager .count{margin-left:0; width:100%; order:-1; margin-bottom:.5rem} }

/* ── single post ──────────────────────────────────────────────────── */
/* The article column keeps its 53rem reading measure; the rail takes the
   width beside it rather than the article growing into it. */
#site .post{padding-block:var(--secpad)}
/* A plain page borrows both — same grid, same measure, same rail — because a
   page like /contribute/ is the same kind of document as a release post and was
   being read at the container's full 74rem. */
#site .postgrid,#site .pagegrid{display:grid; grid-template-columns:minmax(0,1fr) 15.5rem;
  column-gap:clamp(1.75rem,4vw,3.5rem); align-items:start}
#site .postmain,#site .pagemain{min-width:0; max-width:53rem}
/* A page with fewer than three headings has no outline, so it has no rail, so
   it holds no column open for one — and then it takes that width rather than
   sitting centred in it. Two pages of the same kind starting at the container's
   left edge is worth more here than the reading measure: /about/ has three
   headings and /vision/ has two, and centring only the second one made them
   read as different templates. The 53rem measure is the rail's other half;
   with no rail beside it, it is a 21rem gutter holding nothing.

   Keeping the cap here has been tried and reverted. It reads well enough on
   /vision/, which is all paragraphs — but the same rule catches /media/, whose
   content is a carousel and a grid of screencast cards, and neither of those is
   read at a measure. It squeezed them to 848px and put 336px of nothing beside
   them. A page with no rail is not a column, so it is not given one. */
#site .pagegrid--solo{grid-template-columns:minmax(0,1fr)}
#site .pagegrid--solo .pagemain{max-width:none}

#site .crumbs{display:flex; align-items:baseline; gap:.6ch; flex-wrap:wrap;
  font-family:var(--display); font-size:.78rem; color:var(--dim); margin-bottom:1.6rem}
#site .crumbs::before{content:'>'; color:var(--mark); font-weight:600}
#site .crumbs a{text-decoration:none}
#site .crumbs a:hover{color:var(--link)}
#site .crumbs .sep{opacity:.4}
#site .crumbs .here{color:var(--ink)}

#site .phead{padding-bottom:1.6rem; border-bottom:1px solid var(--line); margin-bottom:2rem}
#site .phead__top{display:flex; align-items:center; gap:1ch; flex-wrap:wrap}
#site .verpill{font-family:var(--display); font-size:.7rem; font-weight:600; letter-spacing:.06em;
  color:var(--link); background:var(--band); border:1px solid var(--line);
  border-radius:999px; padding:.16rem .6rem}
#site .phead h1{font-size:clamp(1.7rem,4vw,2.45rem); margin:.85rem 0 1.1rem}
#site .byline{display:flex; align-items:center; gap:.75ch; flex-wrap:wrap;
  font-family:var(--display); font-size:.8rem; color:var(--dim)}
#site .byline .who{color:var(--ink); font-weight:600}
#site .byline .dot{opacity:.4}
#site .avatar{width:2rem; height:2rem; flex:none; margin-right:.35ch; border-radius:50%;
  display:grid; place-items:center; font-size:.68rem; font-weight:600; letter-spacing:.02em;
  color:var(--link); background:var(--band); border:1px solid var(--line)}

/* ── the outline ──────────────────────────────────────────────────
   Contents in the rail, where the rail's stickiness carries them: on a post
   whose changelog runs three screens, an outline that scrolled away with the
   lede was navigation you could only reach before you needed it.

   The copy inside .prose is the narrow-screen half — off by default and
   switched on under the breakpoint where the rail is gone, the same deal
   .pactions strikes. One outline at any width, never two. It sits after the
   lede rather than before it, so the post still opens with a sentence about
   itself rather than with its own navigation. */
#site .prose .outline{display:none; margin-block:1.9rem; padding:1.1rem 1.25rem 1.2rem;
  background:var(--band); border:1px solid var(--line); border-radius:12px}
#site .outline > b{display:block; font-family:var(--display); font-size:.72rem;
  font-weight:500; letter-spacing:.11em; text-transform:uppercase; color:var(--dim);
  padding-bottom:.55rem; margin-bottom:.55rem; border-bottom:1px solid var(--line)}
#site .olist{display:grid; grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));
  gap:.1rem .9rem; counter-reset:o}
/* The rail is 15.5rem, narrower than one auto-fit track asks for: say one
   column rather than leave the grid to overflow its panel. */
#site .outline--rail .olist{grid-template-columns:1fr}
/* In the article these sit inside .prose, whose link rule matches them at
   the same specificity and would win on source order — hence .outline in the
   selector rather than .olist alone, which also lets one rule dress the rail
   copy. These are navigation, not body links: no underline, no accent. */
#site .outline .olist a{display:flex; align-items:baseline; gap:.85ch; counter-increment:o;
  margin-inline:-.5rem; padding:.4rem .5rem; border-radius:7px; font-size:.93rem;
  line-height:1.45; font-weight:400; color:var(--ink); text-decoration:none;
  transition:background .13s, color .13s}
#site .outline .olist a::before{content:counter(o,decimal-leading-zero); flex:none;
  font-family:var(--display); font-size:.74rem; font-weight:600; color:var(--mark)}
#site .outline .olist a:hover{background:var(--card); color:var(--link)}

/* ── prose ────────────────────────────────────────────────────────
   Body copy is --ink here, not --dim: this is the one place on the site
   where someone reads six hundred words rather than scanning forty. */
#site .prose{font-size:1.02rem; line-height:1.75; color:var(--ink)}
#site .prose > * + *{margin-top:1.2rem}
#site .prose .lede{font-size:1.14rem; line-height:1.65}
/* the outline jumps to these, and the header is sticky over them */
#site .prose h2{font-size:clamp(1.22rem,2.6vw,1.45rem); margin-top:2.6rem;
  scroll-margin-top:5.25rem}
#site .prose h3{font-size:1.06rem; margin-top:1.9rem}
#site .prose h2 + p,#site .prose h3 + p{margin-top:.7rem}
#site .prose a{color:var(--link); font-weight:500; text-decoration:underline;
  text-underline-offset:3px; text-decoration-thickness:1px}
#site .prose a:hover{color:var(--orange)}
#site .prose ul,#site .prose ol{margin-left:0; padding-left:1.5rem; list-style:none}
#site .prose ol{counter-reset:n}
#site .prose li{position:relative; margin-top:.5rem}
#site .prose ul li::before{content:'\2013'; position:absolute; left:-1.5rem; color:var(--mark)}
#site .prose ol li{counter-increment:n}
#site .prose ol li::before{content:counter(n) '.'; position:absolute; left:-1.5rem;
  font-family:var(--display); font-size:.85em; color:var(--mark)}
#site .prose code{font-family:var(--display); font-size:.88em; background:var(--band);
  border:1px solid var(--line); border-radius:5px; padding:.1em .4em}
#site .prose figure{margin-block:1.9rem}
/* Screenshots arrive at whatever shape the author took them in. A width:100%
   rule turns a portrait one into a two-screen-tall wall, so the cap is on the
   height and the image centres itself in the measure instead. */
#site .prose figure img{max-width:100%; max-height:28rem; width:auto; margin-inline:auto;
  border:1px solid var(--line); border-radius:12px; background:var(--band)}
#site .prose figcaption{margin-top:.7rem; font-family:var(--display); font-size:.76rem;
  line-height:1.6; color:var(--dim)}
#site .prose figcaption::before{content:'>'; color:var(--mark); margin-right:.6ch}
#site .prose blockquote{margin-block:1.6rem; padding:1.15rem 1.3rem; background:var(--band);
  border-left:3px solid var(--accent2); border-radius:0 10px 10px 0; font-size:.97rem}
#site .prose blockquote p{margin:0}

/* ── /about/, /vision/, /contribute/ ─────────────────────────────
   The live site's words, in the shapes inc/page-layouts.php names: the
   opening as a banner, a feature list as a grid, a run of headed paragraphs
   as cards, steps as steps, credits as a row, a thesis as a pull quote. Each
   class is put there only when the markup already has that shape, so
   anything an editor adds that matches none of them falls back to the prose
   rules above. */
#site .pl-intro{position:relative; overflow:hidden; padding:1.6rem 1.75rem 1.75rem;
  padding-right:clamp(1.75rem,24%,10.5rem); background:var(--band);
  border:1px solid var(--line); border-radius:16px}
/* The mark, cut by the corner: there to say whose page this is, not to be read. */
#site .pl-intro::after{content:''; position:absolute; right:-1.4rem; bottom:-1.6rem;
  width:9.5rem; height:9.5rem; pointer-events:none;
  background:url(../img/mudlet_main_512x512.png) center/contain no-repeat;
  transform:rotate(-8deg)}
#site .pl-intro > * + *{margin-top:.85rem}
#site .prose .pl-claim{font-family:var(--display); font-weight:var(--dw);
  font-size:clamp(1.25rem,2.8vw,1.6rem); line-height:1.3; letter-spacing:var(--dls);
  scroll-margin-top:5.25rem}
#site .pl-claim strong{color:var(--mark)}
#site .prose .pl-lede{font-size:1.06rem; line-height:1.7}
@media (max-width:40rem){
  #site .pl-intro{padding:1.3rem 1.25rem 1.4rem}
  #site .pl-intro::after{display:none}
}

#site .prose ul.pl-feats{display:grid; gap:.55rem; padding-left:0;
  grid-template-columns:repeat(auto-fill,minmax(min(100%,14.5rem),1fr))}
#site .prose .pl-feats li{display:flex; align-items:baseline; gap:.75ch; margin:0;
  padding:.7rem .9rem; background:var(--card); border:1px solid var(--line);
  border-radius:10px; font-size:.95rem; line-height:1.45}
#site .prose .pl-feats li::before{content:'>'; position:static; flex:none;
  font-family:var(--display); font-weight:600}
/* Twelve rows of one short phrase each is a screen and a half of scrolling. */
@media (max-width:40rem){
  #site .prose ul.pl-feats{grid-template-columns:repeat(2,minmax(0,1fr)); gap:.45rem}
  #site .prose .pl-feats li{padding:.6rem .7rem; font-size:.88rem}
}

#site .pl-cards{display:grid; gap:.9rem; counter-reset:card;
  grid-template-columns:repeat(auto-fit,minmax(min(100%,19rem),1fr))}
#site .prose > .pl-cards{margin-top:2.6rem}
#site .pl-card{counter-increment:card; padding:1.2rem 1.3rem 1.3rem; background:var(--card);
  border:1px solid var(--line); border-radius:12px; font-size:.96rem; line-height:1.68}
#site .pl-card > * + *{margin-top:.55rem}
#site .prose .pl-card :is(h2,h3){display:flex; align-items:baseline; gap:.9ch; margin:0;
  font-size:1.08rem}
#site .pl-card :is(h2,h3)::before{content:counter(card,decimal-leading-zero); flex:none;
  font-family:var(--display); font-size:.74rem; font-weight:600; color:var(--mark)}
/* An odd number of cards leaves one alone on the last row; the first one
   takes the whole row instead, and gets the accent for doing it. */
#site .pl-card:first-child:nth-last-child(2n+1){grid-column:1/-1;
  background:var(--band); border-left:3px solid var(--accent2)}

#site .prose ol.pl-steps{padding-left:0}
#site .prose .pl-steps > li{padding-left:2.85rem; min-height:2rem; margin-top:1rem}
#site .prose .pl-steps > li::before{content:counter(n); left:0; top:-.05rem;
  display:grid; place-items:center; width:2rem; height:2rem; border-radius:50%;
  background:var(--orange); color:var(--on-accent); font-size:.82rem; font-weight:600}
#site .prose ol.pl-substeps{margin-top:.8rem; padding:.85rem 1.1rem .95rem 2.6rem;
  background:var(--band); border:1px solid var(--line); border-radius:10px; font-size:.96rem}
#site .prose .pl-substeps li:first-child{margin-top:0}

#site .pl-ico{width:1em; height:1em; flex:none}

/* The download card stays dark in both themes, like the hero terminal: it is
   the one thing on the page that is a way out of it. */
#site .pl-dl{display:flex; align-items:center; justify-content:space-between; gap:1.1rem 1.5rem;
  flex-wrap:wrap; padding:1.4rem 1.5rem; border-radius:16px; color:#e8dcc6;
  background:radial-gradient(120% 140% at 100% 0%, rgba(245,108,39,.28), transparent 55%), #1e1813;
  border:1px solid #3a2f24; box-shadow:0 22px 44px -30px rgba(var(--sh),.7)}
#site .pl-dl__text{display:grid; gap:.45rem; min-width:0}
#site .pl-dl__os{display:flex; gap:.8rem; font-size:1.3rem; color:#9c8f7c}
#site .prose .pl-dl__say{margin:0; font-family:var(--display); font-weight:var(--dw);
  font-size:clamp(1.1rem,2.4vw,1.3rem); letter-spacing:var(--dls); line-height:1.35}
#site .prose .pl-dl__say a{color:#f56c27}
#site .prose .pl-dl__say a:hover{color:#ff8a4d}
#site .pl-dl__when{font-family:var(--display); font-size:.74rem; color:#9c8f7c}
#site .prose .pl-dl__btn{display:inline-flex; align-items:center; gap:.6rem; flex:none;
  padding:.85rem 1.5rem; border-radius:999px; background:#f56c27; color:#17130e;
  font-weight:600; text-decoration:none; box-shadow:0 14px 30px -14px rgba(245,108,39,.6);
  transition:background .13s, transform .13s}
#site .prose .pl-dl__btn:hover{background:#ff8a4d; color:#17130e; transform:translateY(-1px)}

/* A bigger sibling of the credit pills above it, not a rubber stamp. */
#site .prose .pl-stamp{display:inline-flex; align-items:center; gap:.6rem; margin-top:.8rem;
  padding:.5rem 1.1rem .5rem .8rem; border-radius:999px; background:var(--card);
  border:1px solid var(--line); font-family:var(--display); font-weight:600;
  font-size:1.1rem; letter-spacing:.02em}
#site .pl-stamp .pl-ico{font-size:1.3rem; color:var(--mark)}

/* Inline, not inline-flex: a flex link is an atom, and it took its whole
   phrase to the next line rather than break inside it. */
#site .pl-brand .pl-ico{display:inline-block; font-size:.95em; margin-right:.3em;
  vertical-align:-.13em}
#site .pl-card--logos{position:relative}
#site .prose .pl-card--logos :is(h2,h3){padding-right:6.5rem}
#site .pl-logos{position:absolute; top:1.05rem; right:1.2rem; display:flex; gap:.55rem;
  font-size:1.6rem; color:var(--ink)}
/* Each mark in its own colour - in the corner, where it is a logo. On a link
   the icon takes the link's colour, because there it is part of the word. A
   brand's own colour is picked for white, so dark gets a lighter shade of it. */
#site .pl-logos .pl-ico--lua{color:#2c2d72}
#site .pl-logos .pl-ico--cplusplus{color:#00599c}
#site .pl-logos .pl-ico--patreon{color:#f96854}
#site .pl-logos .pl-ico--opencollective{color:#5a8ee6}
#site .pl-logos .pl-ico--paypal{color:#003087}
#site .pl-logos .pl-ico--wiki{color:var(--mark)}
:root[data-theme="dark"] #site .pl-logos .pl-ico--lua{color:#8f91e8}
:root[data-theme="dark"] #site .pl-logos .pl-ico--cplusplus{color:#5aa9e6}
:root[data-theme="dark"] #site .pl-logos .pl-ico--opencollective{color:#7fadf2}
:root[data-theme="dark"] #site .pl-logos .pl-ico--paypal{color:#6c9de0}

#site .prose ul.pl-chips{display:flex; flex-wrap:wrap; gap:.5rem; padding-left:0}
#site .prose .pl-chips li{margin:0}
#site .prose .pl-chips li::before{content:none}
#site .prose .pl-chips a{display:inline-block; padding:.32rem .85rem; border-radius:999px;
  background:var(--card); border:1px solid var(--line); color:var(--ink);
  font-family:var(--display); font-size:.82rem; font-weight:500; text-decoration:none;
  transition:border-color .13s, color .13s}
#site .prose .pl-chips a:hover{border-color:var(--accent2); color:var(--link)}

/* Vision is a letter, and has no h2 to hang a rail off, so it would be read
   at the container's full 74rem; it is held at a measure instead, from the
   same left edge as its parent. flow-root keeps the floated picture in. */
#site .prose--vision{max-width:46rem; display:flow-root}
#site .prose .pl-pull{position:relative; margin-block:2.4rem; padding:1.5rem 0 1.5rem 3.3rem;
  border-block:1px solid var(--line); font-family:var(--display); font-weight:var(--dw);
  font-size:clamp(1.18rem,2.6vw,1.45rem); line-height:1.42; letter-spacing:var(--dls)}
#site .pl-pull::before{content:'\201C'; position:absolute; left:0; top:1.1rem;
  font-size:4.2rem; line-height:1; color:var(--mark)}
#site .prose .pl-shot{margin-block:1.9rem}
#site .prose .pl-shot img{display:block; width:100%; height:auto; max-height:none;
  border:1px solid var(--line); border-radius:12px; background:var(--band);
  box-shadow:0 18px 40px -26px rgba(var(--sh),.5)}
#site .prose .pl-shot--right{float:right; width:min(48%,22rem); margin:.4rem 0 1rem 1.6rem}
#site .prose .pl-shot--left{float:left; width:min(48%,22rem); margin:.4rem 1.6rem 1rem 0}
#site .prose .pl-sign{clear:both; margin-top:2.2rem; text-align:right;
  font-family:var(--display); font-weight:600; color:var(--mark)}
@media (max-width:40rem){
  #site .prose .pl-pull{padding-left:2.4rem}
  #site .pl-pull::before{font-size:3.2rem}
  #site .prose :is(.pl-shot--right,.pl-shot--left){float:none; width:auto; margin:1.6rem 0}
}

/* ── changelog ────────────────────────────────────────────────────
   The one block on the page that stays dark in both themes, for the same
   reason the hero terminal does: this is console output, and Mudlet's own
   changelog is read in a console. Issue numbers are links, in the terminal
   palette's cool, so a wall of fixes still has a way in. */
#site .chlog{--c-bg:#1e1813; --c-bar:#2b231b; --c-line:#3a2f24; --c-fg:#e8dcc6;
  --c-dim:#9c8f7c; --c-hot:#f56c27; --c-cool:#82c0c7; --c-sys:#8a9a5b;
  margin-block:1.9rem; background:var(--c-bg); border:1px solid var(--c-line);
  border-radius:12px; overflow:hidden; color:var(--c-fg);
  font-family:var(--display); font-size:.83rem; line-height:1.75}
#site .chlog__bar{display:flex; align-items:center; gap:.6ch; padding:.5rem .9rem;
  background:var(--c-bar); border-bottom:1px solid var(--c-line);
  font-size:.7rem; letter-spacing:.04em; color:var(--c-dim)}
#site .chlog__bar .mk{color:var(--c-hot)}
#site .chlog__bar .n{margin-left:auto}
#site .chlog__body{margin:0; padding:1rem 1.1rem 1.15rem; overflow-x:auto}
#site .chlog dt{margin-top:1.1rem; font-size:.7rem; letter-spacing:.13em; text-transform:uppercase;
  color:var(--c-sys)}
#site .chlog dt:first-child{margin-top:0}
#site .chlog dd{position:relative; margin:0 0 0 1.7ch; color:var(--c-dim)}
#site .chlog dd::before{content:'-'; position:absolute; left:-1.7ch; color:var(--c-hot); opacity:.75}
#site .chlog a{color:var(--c-cool); text-decoration:none}
#site .chlog a:hover{text-decoration:underline; text-underline-offset:3px}

/* Its scrollbar, too. The body scrolls — sideways here, and down the four
   hundred lines of a real changelog in WordPress, which caps its height — and
   left alone the browser draws that bar in the page's colours, so a light rail
   runs down the dark box. Paint it in the terminal palette instead: thin, the
   header bar as its track, a thumb a shade above the border. The standard pair
   is set on the widget and inherits to whatever inside it scrolls; the -webkit-
   rules say the same thing for Safari, which has neither of them (Chrome
   ignores them once scrollbar-color is set). */
#site .chlog{scrollbar-width:thin; scrollbar-color:#5b4a3a var(--c-bar)}
#site .chlog__body::-webkit-scrollbar{width:11px; height:11px}
#site .chlog__body::-webkit-scrollbar-track{background:var(--c-bar)}
#site .chlog__body::-webkit-scrollbar-thumb{background:#5b4a3a; border-radius:99px;
  border:3px solid var(--c-bar)}
#site .chlog__body::-webkit-scrollbar-thumb:hover{background:#75604b}

/* ── contributors ────────────────────────────────────────────────
   Who the changelog above is by. Same source — the compare between
   two tags knows the author of every commit in it — so this sits
   directly under it, wearing the same bar.

   Light, though, where the changelog is dark. That block is console
   output and reads like it; this one is a list of people, and a
   second dark slab would turn the pair into one undifferentiated
   wall. The chips carry the avatar where there is one and the
   person's initials where there is not, so a contributor GitHub
   could not match to an account still gets a face-shaped space
   rather than a gap. */
#site .credits{margin-block:1.9rem; background:var(--card); border:1px solid var(--line);
  border-radius:12px; overflow:hidden}
#site .credits__bar{display:flex; align-items:center; gap:.6ch; padding:.45rem .9rem;
  background:var(--band); border-bottom:1px solid var(--line);
  font-family:var(--display); font-size:.7rem; letter-spacing:.04em; color:var(--dim)}
#site .credits__bar .mk{color:var(--mark)}
#site .credits__bar .n{margin-left:auto}
/* Doubled class, not for fun: this block lives inside .prose on a post, and
   #site .prose ul / .prose ul li::before are more specific than a single class
   would be. Prose hangs an en dash off every list item and indents the list;
   these are chips in a row, not a list of points. */
#site .credits .credits__body{list-style:none; margin:0; padding:.6rem .65rem;
  display:flex; flex-wrap:wrap; gap:.3rem}
#site .credits .credits__body li{margin:0; padding:0; position:static}
#site .credits .credits__body li::before{content:none}
/* Borderless: at twenty-odd people a ring around every chip is twenty-odd
   more lines than the block needs, and the avatar already gives each one an
   edge. The hover tint is what marks a chip as a link. */
#site .credits__p{display:flex; align-items:center; gap:.4rem;
  padding:.12rem .5rem .12rem .12rem; border-radius:999px;
  text-decoration:none; transition:background .15s}
#site a.credits__p:hover{background:var(--band)}
#site .credits__av{width:1.25rem; height:1.25rem; flex:none; border-radius:50%; overflow:hidden;
  display:grid; place-items:center; background:var(--band); color:var(--dim);
  font-family:var(--display); font-size:.5rem; letter-spacing:.02em}
#site .credits__av img{width:100%; height:100%; object-fit:cover; display:block}
#site .credits__p b{font-size:.78rem; font-weight:500; color:var(--ink)}
#site .credits__p .c{font-family:'IBM Plex Mono',ui-monospace,monospace; font-size:.66rem;
  color:var(--dim); font-variant-numeric:tabular-nums}

/* ── the post rail ────────────────────────────────────────────────
   The contents first, then what a release post is actually for: the
   build, and the places to talk about it. Sticky, so the outline and the
   download both stay in reach through three screens of changelog.

   Which is why the end-of-article button row below is off by default and
   comes back only under the breakpoint, where the rail is gone — the same
   deal the archive strikes between its chip row and its category list.
   One set of actions at any width, never two. */
#site .prail{position:sticky; align-self:start; top:4.6rem;
  display:flex; flex-direction:column; gap:1.6rem}
/* the archive's release block, unframed: the panel around it is frame enough */
#site .prail .relbox{border:0; background:transparent; padding:0}
#site .prail .relbox dl{border-top-color:var(--line)}
/* The panel's button goes to /download/, which is always the newest build —
   so it says so, rather than naming this post's version and sending somebody
   after a build that page will not hand them. Naming the version is the ghost
   button's job: that release's own page on GitHub, assets and all. */
#site .prail .btn{width:100%; justify-content:center; margin-top:1.05rem; font-size:.92rem}
#site .prail .btn + .btn{margin-top:.55rem}

/* ── what to do next ─────────────────────────────────────────────── */
#site .pactions{display:none; gap:.7rem; flex-wrap:wrap; align-items:center;
  margin-top:2.25rem; padding-top:1.75rem; border-top:1px solid var(--line)}

@media (max-width:68rem){
  #site .postgrid,#site .pagegrid{grid-template-columns:1fr}
  /* with the rail gone the container narrows too, so the article is centred
     in the page rather than pinned to the left of a 74rem box. The page does
     it to the grid rather than to .w, because .page--page is also /games/,
     /contact/, a game, a maker and the 404, and none of those is a column. */
  #site .post .w{max-width:53rem}
  /* A page that never had a rail is full width at every size, so it is not one
     of the ones losing one here. */
  #site .pagegrid:not(.pagegrid--solo){max-width:53rem; margin-inline:auto}
  /* Everything the rail holds has a home in the article at this width: the
     version and date are in the header, the counts are in the changelog, and
     the outline and the actions come back into the column. Nothing is lost
     by dropping it. */
  #site .prail{display:none}
  #site .prose .outline{display:block}
  #site .pactions{display:flex}
}
#site .postnav{display:grid; grid-template-columns:1fr 1fr; gap:.9rem; margin-top:2.25rem}
#site .pnav{display:flex; flex-direction:column; gap:.35rem; padding:1rem 1.15rem;
  border:1px solid var(--line); border-radius:12px; text-decoration:none; transition:.15s}
#site .pnav:hover{border-color:var(--accent2); background:var(--band)}
#site .pnav span{font-family:var(--display); font-size:.71rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--dim)}
#site .pnav b{font-family:var(--display); font-weight:600; font-size:.95rem; line-height:1.35}
#site .pnav--next{align-items:flex-end; text-align:right}
@media (max-width:44rem){
  #site .postnav{grid-template-columns:1fr}
  #site .pnav--next{align-items:flex-start; text-align:left}
}
