/* oomoo -- the home page. This file is the page.
 *
 * It is also the entry point: the imports below are the whole dependency list, and the
 * order is the contract. Tokens first, since everything after reads --ink, --line and
 * --text; then the reset and the faces; then the ambient layers, which style nothing this
 * file touches. Imported sheets sit below this file in the cascade, which is what we want
 * -- the page has the last word.
 *
 * Everything that styles content lives here, in the order you meet it: shell, masthead,
 * content row, rail, panel, journal, favorites, buttons, footer. The rail comes early
 * because it's the other column of the row above it rather than anything to do with the
 * panel, and everything from journal down is inside the panel. What sits in its own file is
 * the stuff that gets set once and then left alone.
 *
 * Mobile-first throughout: what's outside a media query is the phone, and every query is a
 * min-width that adds to it. Nothing is ever taken back -- a wide rule may replace a narrow
 * value, but no rule exists only to undo one.
 *
 * The ladder, in order, and the same five rungs the imported sheets use:
 *
 *    480px  the page's side margin opens from 14px to 24px
 *    700px  the panel gets its full padding, and everything laid out in columns takes them
 *    768px  the ruler appears, so the page stands off the left edge to clear it, the
 *           crosshair field tightens, and the masthead's chip and splash go back to hanging
 *           off the wordmark's corners
 *   1120px  the rail leaves the foot of the page for the right gutter, and takes the nav
 *           with it
 *   1360px  there are gutters wide enough for the contour speckle to sit in
 *
 * Each block keeps its own media query rather than collecting them at the bottom, so a
 * component's responsive behaviour is never more than a few lines from the component.
 */

@import url('../tokens.css');
@import url('../reset.css');
@import url('../utilities.css');
@import url('./fonts.css');

@import url('./crosshairs.css');
@import url('./constellation.css');
@import url('./ruler.css');
@import url('./theme-toggle.css');
@import url('./lightbox.css');

/* ------------------------------------------------------------------- Type */

/* The reading size, and the only knob for it.
 *
 * Everything the page sets in rem -- prose, the panel's padding, the gaps between cards --
 * is a multiple of this one figure, so the page's reading size moves from here without
 * touching a single component. The layout is px (--panel-w and the ladder around it), so
 * raising this sets bigger type inside the same columns rather than making the page wider.
 *
 * 112.5% is 18px against the browser's 16. A percentage rather than a px value because a
 * visitor who has already raised their own default should keep the increase.
 *
 * The mono readouts are the exception and stay in px, on their own ladder of 9/10/11/12.
 * They're chrome -- labels, badges, timestamps -- and chrome that grows with the prose
 * stops reading as chrome. The 7px inside the 88x31 button is exempt from even that ladder,
 * for the same reason the button is 88x31.
 */

/* ------------------------------------------------------------------- Page */

body {
  min-height: 100vh;
  background: var(--bg);
  color: var(--ink);
  font-family: 'National Park', system-ui, sans-serif;
  line-height: 1.5;
}

/* The gutter
 *
 * The one measurement every full-width thing on the page stands off the edge by. It's a
 * variable rather than three copies of a ladder because the footer needs the same figure as
 * the page body -- and because it must be symmetric and identical on both, or the centered
 * wordmark, panel and footer stop agreeing about where the middle is.
 *
 * On a phone it's a plain margin and nothing more: there's no ruler yet, and the panel brings
 * its own padding inside it, so it stays as narrow as the content can bear. From 768px it also
 * has to clear the ruler, which is 52px of the 84.
 *
 * On :root rather than in tokens.css for the same reason the crosshair geometry stays in its
 * own file: that sheet is the palette, and this is a measurement. */

:root {
  --gutter: 14px;

  /* The content row's four numbers, and the whole of its layout. --cluster-half is the only
     derived one and the only one the speckle reads; keep it in step with the other three. Up
     here rather than on .content because the footer lines its width up with the row's, so both
     have to measure from the same figures. */
  --rail-w: 200px;
  --rail-gap: 20px;
  --panel-w: 726px;
  --cluster-half: 473px; /* (panel + gap + rail) / 2 */

  /* How much of --bg shows through the panel, the rail's cards and the footer; the rest lets
     the crosshairs come through. One knob, so the surfaces can't drift apart. */
  --surface-alpha: 85%;
}

@media (min-width: 480px) {
  :root {
    --gutter: 24px;
  }
}

@media (min-width: 768px) {
  :root {
    --gutter: 84px; /* the 52px ruler, plus room */
  }
}

.page,
.foot {
  padding-inline: var(--gutter);
}

/* Links
 *
 * One look for every link on the site: the ink, underlined in the same ink. Set on the element
 * rather than per component, because there were four component rules saying nearly this and one
 * -- the manifesto's, the only prose links here -- that had been left to the browser and came
 * out in blue.
 *
 * currentColor rather than --line, so the rule under a word is the same weight as the word.
 * Offset far enough that descenders clear it; any closer and the underline reads as part of the
 * letterforms rather than under them.
 *
 * The hover is the invert the nav and the copy button already use, and it drops the underline,
 * which has nothing to sit on once the word is knocked out of a solid block.
 *
 * The nav is the exception, and it says so itself (.nav a, in the Masthead section): its links
 * are a strip of labels rather than words in a sentence, and nine underlines in a row read as a
 * fence. */

a {
  color: var(--ink);
  text-decoration-line: underline;
  text-decoration-color: currentColor;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover {
  background: var(--ink);
  color: var(--bg);
  text-decoration: none;
}

/* .constellation moves with the field by definition -- if one stops, both must, or the
   lit marks drift off the real ones. */
@media (prefers-reduced-motion: reduce) {
  body::before,
  .constellation,
  .ruler__track {
    transform: none;
  }
}

/* --------------------------------------------------------------- Masthead */

/* The wordmark with the visitor chip and the splash line stacked under it, and from 768px up
   those two hang off its corners instead -- see the Cluster block below.
 *
 * The top padding is for the theme toggle, which is pinned to the top right corner and would
   otherwise land on the wordmark: on a phone the wordmark is nearly the width of the screen,
   so there is no corner left over for the toggle to sit in. By 768px there is. */
.head {
  padding-top: 2.5rem;
  text-align: center;
}

/* Fluid the whole way, with no step in it: 10vw from the floor at ~360px to the ceiling at
   ~1120px, and flat either side. The floor matters more than it looks -- a fixed one big
   enough to be a wordmark on a laptop is wider than a phone, and the glyphs run off the edge
   rather than wrapping, because the wordmark is only as wide as the cluster lets it be. */
.logo {
  display: block;
  margin: 0 auto;
  padding: 0 1.5rem;
  color: var(--ink);
  font-family: 'Terminal Grotesque', sans-serif;
  font-weight: 400;
  font-size: clamp(2.25rem, 10vw, 7rem);
  line-height: 1;
}

@media (min-width: 768px) {
  .head {
    padding-top: 0;
  }
}

/* Cluster
 *
 * Two things belong to the wordmark: the visitor chip and the splash line. From 768px up they
 * hang off its bottom corners, which is the design -- a chip clipped to the left edge, a
 * splash slanting off the right, the way a title screen does it.
 *
 * A phone has no corners to spare. The wordmark fills the width, so both of them would sit on
 * top of the letterforms and hang off the screen, which is what they did. So below 768px they
 * are simply in the flow, centered, in the order they're written: wordmark, chip, splash.
 * Same three things, stacked instead of pinned.
 *
 * That's what .logo-cluster's display switch is for. Shrink-to-fit is what lets the two
 * pinned offsets measure from the wordmark's own corner rather than the page's, and it's also
 * what squeezes the wordmark on a narrow screen, so it only applies where the pinning does. */

.logo-cluster {
  position: relative;
  display: block;
}

.chip {
  display: block;
  width: fit-content;
  padding: 0.35em 0.7em;
  background: var(--hud-green);
  color: var(--bg);
  font: 500 11px/1 var(--chrome-face);
  letter-spacing: 0.14em;
  white-space: nowrap;
}

/* A button, because clicking it signs the page. It keeps the chip's look rather than a
   button's -- .chip's own background and padding beat the reset's. */
.chip--visitor {
  margin: 0.7rem auto 0;
  cursor: pointer;
}

/* The field that replaces the label while you're signing. Sized in ch so the chip barely
   moves as it swaps, and inheriting the chip's type so the two look like one thing -- all of
   it but the casing, which the chip's .label would otherwise pass down to what you type. */
.chip__input {
  width: 12ch;
  border: 0;
  border-bottom: 1px solid var(--bg);
  background: none;
  color: var(--bg);
  font: inherit;
  letter-spacing: inherit;
  text-transform: none;
}

.chip__input:focus {
  outline: 0;
}

/* Splash text
 *
 * The yellow line on a slant off the corner of a game title screen, copied closely enough to
 * be recognised: Minecraft's own face, its tilt, its shadow, and its bob.
 *
 * The bob is the real detail. The game recomputes the scale every frame as
 * 1.8 - |sin(t / 1000 * 2pi)| * 0.1 -- one second per cycle, but the absolute value folds it
 * into two dips a second, and the range works out to 5.6% of the size. That's the keyframes
 * below: dips at 25% and 75%, eased so the corners read as a sine rather than a bounce.
 *
 * The colour is a literal rather than a palette token, and it's the one exception to
 * tokens.css owning every colour on the site: this isn't the design's yellow, it's
 * Minecraft's, and re-tinting it to match the theme is what would break the joke. The
 * shadow isn't a second literal -- the game paints it as the text colour at quarter
 * brightness, so that's how it's derived, and a theme only ever has to set the one value. */

.splash {
  --splash-tilt: -11deg; /* the game's own angle */
  --splash-ink: #ffff00;
  --splash-shadow: color-mix(in srgb, var(--splash-ink) 25%, black);

  display: block;
  width: fit-content;
  margin: 0.55rem auto 0;

  color: var(--splash-ink);
  font:
    400 clamp(11px, 2vw, 20px) / 1 'Minecraft',
    var(--chrome-face);
  letter-spacing: normal; /* a pixel face carries its own spacing */
  white-space: nowrap;
  text-shadow: 1px 1px 2px var(--splash-shadow);
  pointer-events: none;

  animation: splash-bob 1s ease-in-out infinite;
}

/* Pure yellow on white is a smudge rather than text, but the fix is a small one: the green
   channel down a third and nothing else moved. It's still plainly the splash yellow -- go
   any further and it turns gold, which is both muddy against the shadow and a near miss for
   the site's own accent (--accent, #b07607). What actually carries the letterforms on white
   isn't the ink's contrast, it's the shadow, and that follows the ink down on its own. */
:root[data-theme='light'] .splash {
  --splash-ink: #ffc400;
}

@keyframes splash-bob {
  0%,
  50%,
  100% {
    transform: rotate(var(--splash-tilt)) scale(1);
  }

  25%,
  75% {
    transform: rotate(var(--splash-tilt)) scale(0.98);
  }
}

/* Still tilted, just still. The tilt is the look; the bob is the motion. */
@media (prefers-reduced-motion: reduce) {
  .splash {
    animation: none;
    transform: rotate(var(--splash-tilt));
  }
}

/* Cycling visual
 *
 * `contain` rather than a percentage pair: the cutouts range from wide (474x192) to tall
 * (513x700), and scaling both axes to the box stretched every one of them. contain fits each
 * at its own ratio, as large as the box allows. mask-origin keeps it off the border.
 *
 * The CRT scanlines are part of the paint rather than an overlay, so the mask clips them to
 * the artwork itself instead of striping the whole frame. */

.visual {
  width: 100%;
  max-width: 390px;
  aspect-ratio: 4 / 3;
  margin: 2.5rem auto 0;
  /* Heavier bottom padding: mask-origin is the content box, so this lifts the cutout off the
     lower edge rather than just padding the frame. */
  padding: 14px 14px 38px;
  border: 1px solid var(--line);
  cursor: pointer;

  background-color: var(--accent);
  background-image: repeating-linear-gradient(0deg, transparent 0 2px, rgb(0 0 0 / 0.5) 2px 3px);
  mask-image: var(--visual-src);
  mask-size: contain;
  mask-origin: content-box;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-image: var(--visual-src);
  -webkit-mask-size: contain;
  -webkit-mask-origin: content-box;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
}

/* Two navs, one table (see pages/home.njk), never both at once. This strip is the nav for as
   long as the rail is stacked at the foot of the page, where a nav would be no use; once the
   rail moves into the gutter it takes the nav with it and the query in the Rail section puts
   this away. The link rules below are shared by both. */

.nav--head {
  position: relative;
  display: flex;
  margin: -0.9rem auto 0;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.9rem;
  width: fit-content;
  max-width: 100%;
  padding: 0.55rem 1rem;
  background: var(--bg);
  border: 1px solid var(--line);
}

/* The one place a link isn't underlined -- see the Links block in the Page section. These are a
   strip of labels, not words in a sentence, and nine rules in a row read as a fence. */
.nav a {
  font: 500 11px/1 var(--chrome-face);
  letter-spacing: 0.14em;
  text-decoration: none;
}

/* The coming-soon banner riding next to Shrines/Links/Contact (href-less <a>s, so every
   nav rule above still applies). inline-block because the reset blocks every img, which
   would drop the banner onto its own line. */
.nav__soon-gif {
  display: inline-block;
  height: 13px;
  width: auto;
  margin-left: 0.35em;
  vertical-align: middle;
  image-rendering: pixelated;
}

/* Pinned
 *
 * The page has margins to hang things into from here up, so the cluster shrinks to the
 * wordmark and the chip and splash take their corners. Both offsets are anchored to that
 * corner rather than to the page, so they follow the wordmark as it grows with the viewport
 * instead of drifting away from it -- the splash clear of the letterforms on the right, the
 * chip lapped over the left edge. */

@media (min-width: 768px) {
  .logo-cluster {
    display: inline-block;
  }

  .chip {
    position: absolute;
  }

  .chip--visitor {
    margin: 0;
    bottom: -0.6em;
    left: -1.5rem;
  }

  .splash {
    position: absolute;
    margin: 0;
    right: -4rem;
    bottom: -0.8em;
  }
}

/* ---------------------------------------------------------------- Content */

/* The content row: the panel, the rail in the right gutter, the speckle in the two gutters
   left over. The pair is centered as one, so the masthead above -- which is centered on the
   page -- still sits over the middle of it.
 *
 * The four numbers it lays out on are in :root -- the footer squares up with them too.
 *
 * The contours' opacity knob (--deco-scale) lives in tokens.css: it's set per theme, and a
 * declaration here would shadow that for everything inside .content. */
.content {
  position: relative; /* anchors the contour speckle in the gutters */

  /* One column until there's room for two: the panel, then the rail beneath it. The track
     is capped rather than fixed, so it's the panel's width on a laptop and the screen's on a
     phone without a second rule saying so. */
  display: grid;
  grid-template-columns: minmax(0, var(--panel-w));
  justify-content: center;
  align-items: start;
  gap: 2rem;
  margin-block: 2rem 4rem;
}

/* Contour speckle
 *
 * The sources were black line art on white paper. They're converted to PNGs whose ALPHA is
 * the linework (public/media/contours), so the paper is gone and CSS can use each one as a
 * mask -- the color comes from --accent underneath, not from the artwork.
 *
 * They sit in the gutters either side of the content, at low opacity, and only once those
 * gutters are wide enough to hold them -- below that there's nothing to sit in, so they stay
 * off rather than crowd the panel.
 *
 * Which image goes where is the Speckle table in pages/home.njk -- one row per mark,
 * carrying its source, size, side, offset, rotation and opacity as inline custom properties.
 * Placement is data, so rearranging the speckle never touches this file. */

.deco {
  display: none;
  position: absolute;
  width: var(--deco-size, 120px);
  aspect-ratio: var(--deco-ratio, 1);
  transform: rotate(var(--deco-rotate, 0deg));
  pointer-events: none;

  background-color: var(--accent);
  opacity: calc(var(--deco-opacity, 0.16) * var(--deco-scale, 1));

  mask-image: var(--deco-src);
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-image: var(--deco-src);
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
}

/* Anchored to the cluster's outer edges, not the page's, so the speckle hugs the content
   however wide the viewport gets: the panel's left edge on one side, the rail's right edge on
   the other. --deco-off is the distance out from that edge; negative laps the mark over the
   content. */

.deco--l {
  right: calc(50% + var(--cluster-half) + var(--deco-off, 0px));
}

.deco--r {
  left: calc(50% + var(--cluster-half) + var(--deco-off, 0px));
}

/* The widest mark reaches ~190px past the cluster's edge, and the cluster is 946px, so this
   is the first width with room for the pair of them either side. Any narrower and the speckle
   would clip off the sides rather than sit in them. */
@media (min-width: 1360px) {
  .deco {
    display: block;
  }
}

/* ------------------------------------------------------------------ Badge */

/* The HUD tag: a few words in a tight outlined box, lit, in amber. Straight off the entry-plug
 * displays the crosshair field and the ruler already come from -- OBJECT: 15th ANGEL, RESERVE
 * ENERGY REMAINING, BORDER LINE -- where anything the machine wants read is boxed and lit
 * rather than merely set.
 *
 * Amber rather than ink because those displays are amber, and because on black it's the one HUD
 * colour that outreads the ink: ~10:1 against red's 5.3. In the light theme --hud-amber and
 * --accent are already the same value, so that theme is unchanged.
 *
 * This is the only place the HUD palette is used as text -- tokens.css says it's for trails and
 * never for reading, and that still holds everywhere else. It's allowed here because a badge is
 * short, uppercase, and always sits on the page's own background rather than over artwork.
 *
 * The glow is the display's bloom kept to a few pixels. The references are a photograph of a
 * CRT and this is a web page: the same effect at the same strength just looks out of focus.
 *
 * Nothing wears it at the moment -- the rail's card titles did for a while and went back to the
 * plain legend. It's kept because the component is right even where it wasn't wanted, and the
 * page has key/value pairs waiting for it: the webring's member number, the shelf count, the
 * readout's figures.
 */

.badge {
  --badge-ink: var(--hud-amber);

  display: inline-block;
  padding: 0.3em 0.6em;
  border: 1px solid var(--badge-ink);
  background: var(--bg);
  color: var(--badge-ink);
  font: 500 10px/1 var(--chrome-face);
  letter-spacing: 0.16em;
  white-space: nowrap;
  text-shadow: 0 0 5px color-mix(in srgb, var(--badge-ink) 55%, transparent);
  box-shadow: 0 0 5px -1px color-mix(in srgb, var(--badge-ink) 45%, transparent);
}

/* Two rules instead of one -- the RESERVE ENERGY panel's own detail, for a badge that has to
   outrank its neighbours. outline rather than a second border, so the extra rule costs no
   layout and the badge stays the same size either way. */
.badge--double {
  outline: 1px solid var(--badge-ink);
  outline-offset: 2px;
}

/* Key and value in one tag, the way OBJECT: 15th ANGEL splits: the key filled and knocked out,
   the value left open. The negative margins bleed the key back out to the badge's own edges,
   which its padding then re-insets -- so the fill meets the border with no seam. */
.badge__key {
  margin: -0.3em 0.6em -0.3em -0.6em;
  padding: 0.3em 0.5em;
  background: var(--badge-ink);
  color: var(--bg);
  text-shadow: none;
}

/* ------------------------------------------------------------------- Rail */

/* The cards -- see the Rail block in pages/home.njk for what's in them and why every one is
 * a stub. Where they sit depends on the width: a band under the panel until 1120px, a column
 * in the right gutter above it.
 *
 * Columns rather than a grid, because these cards are wildly unequal heights -- a status light
 * against a six-line shelf -- and a grid rows them up, so every row is as tall as its tallest
 * card and the short ones sit in a hole. Multi-column packs instead: each card falls to the
 * bottom of the shortest column, and `balance` (the default) evens the columns out. Masonry,
 * with no JS and no measuring.
 *
 * `columns: 176px` is a width, not a count -- the browser fits as many as it can, which is one
 * card in the gutter and two to four across when the rail has the panel's width to spread
 * over. No card knows which of the two it's in.
 *
 * The cost is reading order: columns run top-to-bottom before left-to-right, so the order down
 * the page is the order down a column, not across a row. For a rail of ambient widgets that's
 * no loss -- there's no sequence to lose -- and the nav card, the one thing here with an order
 * that matters, is a list inside a single card either way.
 *
 * (`grid-template-rows: masonry` would do this in one line and keep row order. It isn't
 * shipped anywhere we can rely on; when it is, this is the block to revisit.)
 */

.rail {
  columns: 176px;
  column-gap: 1rem;
  margin-bottom: -1rem; /* cancels the last card's, which has no card after it to space from */
}

/* Cards space themselves, since column layout has no gap between rows, and break-inside is
   what keeps one whole rather than letting a column boundary cut through the middle of it.
   The surface is the panel's, so the crosshair field carries through the cards at the same
   strength and the two columns read as one plane rather than two. */
.card {
  break-inside: avoid;
  margin-bottom: 1rem;

  background: color-mix(in srgb, var(--bg) var(--surface-alpha), transparent);
  border: 1px solid var(--line);
}

/* The title rides the card's own top rule rather than sitting in a bar beneath it -- a legend
   on a fieldset, a label on a schematic. Same trick either way: the heading is pulled up by
   half its own height and paints an opaque background over the line behind it, so the rule
   appears to break and let it through.
 *
 * Worth the two negative numbers. A bar costs a second hairline per card, and eight cards each
   drawing three lines is what made a column of them read as a stack of boxes rather than as a
   margin, which is the rail's actual job.
 *
 * The background is the page's, not the card's 85% of it: what's behind the title is a hairline
   that has to disappear, and 15% of one still showing reads as a smudge. Against black the two
   are indistinguishable and against white they're the same value, so nothing is given up.
 *
 * Accent rather than ink, like the ruler and the speckle: these are chrome, and a label the
   same colour as the thing it labels has to work harder to read as one. */
.card__title {
  width: fit-content;
  margin: -0.55em 0 0.2rem 0.7rem;
  padding: 0 0.45rem;
  background: var(--bg);
  color: var(--accent);
  font: 500 10px/1 var(--chrome-face);
  letter-spacing: 0.16em;
}

.card__body {
  display: grid;
  gap: 0.5rem;
  padding: 0.45rem 0.6rem 0.6rem;
}

.card__body p {
  color: var(--text);
  font-size: 0.8rem;
}

/* The line under a card is a footnote, not a label, and it was being set like one -- 9px caps
   at 0.14em, which at two wrapped lines in a 200px column reads as loud as the thing it's
   annotating. The badge is the card's label now; this is what the machine printed underneath.
 *
 * So: no .label in the markup, which leaves the copy in the case it was written in, a little
   larger to survive losing the caps, and the tracking almost out -- a readout's letterspacing
   exists to hold capitals apart and does nothing but scatter a lowercase line. Reads as log
   output, which is what "scrobbled 4 hours ago" is. */
.card__body .tag {
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.01em;
  line-height: 1.55;
}

/* Readout
 *
 * Rows of label left, figure right, on one line -- the one card whose contents arrive from the
 * server rather than being written into the page. */

.readout li {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  /* The two halves of a row are different faces at different sizes -- 16px prose against
     10px mono -- so without this they stretch to the row's full height and each sets its
     own line at the top of it, which hangs the figure above the label. Centred rather than
     baseline: on a size gap this wide, sitting the numerals on the prose's baseline drops
     them to the bottom of the row. */
  align-items: center;
}

/* The figures are green, not ink -- the one HUD colour used as text on the page. It marks
   them as the numbers that came from the server rather than the ones that were written into
   it. See --readout-figure in css/tokens.css for why it isn't --hud-green directly. */
.readout span {
  color: var(--readout-figure);
  font: 500 10px/1.6 var(--chrome-face);
  letter-spacing: 0.1em;
  white-space: nowrap;
}

/* Now playing */

/* The art carries the card: full width, square, with the track under it. */
.np {
  display: grid;
  gap: 0.55rem;
}

.np__meta {
  display: grid;
  gap: 0.15rem;
  min-width: 0;
}

.np__track,
.np__artist {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.np__track {
  color: var(--ink);
  font-size: 0.85rem;
}

.np__artist {
  color: var(--dim);
  font: 500 11px/1.35 var(--chrome-face);
  letter-spacing: 0.01em;
}

/* Real album art slid under the meter when the API has something. Straight off
   Spotify's CDN, undithered -- it can change every three minutes and the plate is
   46px. Dimmed so the bars stay legible on top of it. */
.cover--square .np__art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  image-rendering: pixelated; /* dithered art: whole pixels or nothing */
  /* Less dimming than the 46px plate needed -- at full width the bars only share a
     corner with the art, and the cover is the point now. */
  opacity: 0.8;
}

/* Nothing on: the meter holds still instead of pretending. */
.np--paused .eq i {
  animation: none;
}

/* Three bars on the plate, out of step with each other so they never look like one shape
   moving. Decorative and aria-hidden, so it stops dead under reduced motion with nothing
   lost -- the track name is what carries the information. */

.eq {
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 13px;
}

.eq i {
  width: 3px;
  height: 100%;
  background: var(--ink);
  transform: scaleY(0.35);
  transform-origin: bottom;
}

@media (prefers-reduced-motion: no-preference) {
  .eq i {
    animation: eq 900ms ease-in-out infinite alternate;
  }

  .eq i:nth-child(2) {
    animation-duration: 640ms;
  }

  .eq i:nth-child(3) {
    animation-duration: 1180ms;
  }
}

@keyframes eq {
  from {
    transform: scaleY(0.2);
  }

  to {
    transform: scaleY(1);
  }
}

/* Contents
 *
 * The same links as the masthead strip, stacked. No .card__body around it: the rows carry
 * their own padding so a hover lights the whole width of the card rather than the label. */

.nav--rail {
  display: grid;
  padding-bottom: 0.15rem;
}

.nav--rail a {
  padding: 0.42rem 0.6rem;
  border-bottom: 1px dotted var(--line);
}

.nav--rail a:last-child {
  border-bottom: 0;
}

/* Hotlinking
 *
 * The field is readonly rather than a <code> block so it can be selected in one gesture and
 * so the copy button has something to read. That button is scripts/home/hotlink.js; with the
 * script blocked the field is still there and still selectable, which is how this worked for
 * twenty years before clipboard APIs. */

.card--hotlink .btn88 {
  justify-self: center;
}

.embed {
  display: grid;
  gap: 0.3rem;
}

.embed__field {
  width: 100%;
  padding: 0.35rem 0.4rem;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--dim);
  font: 500 10px/1.4 var(--chrome-face);
}

.embed__copy {
  padding: 0.4rem;
  border: 1px solid var(--line);
  background: none;
  color: var(--ink);
  font: 500 10px/1 var(--chrome-face);
  letter-spacing: 0.14em;
  cursor: pointer;
}

.embed__copy:hover {
  background: var(--ink);
  color: var(--bg);
}

/* Ad-space
 *
 * Dashed rather than solid, the way an empty slot has been drawn since the banner era: the box
 * is the offer, and the dashes are what say nobody has taken it. It sits directly above the
 * wall on purpose -- one is the space I'd give away, the other is what came back. */

.adspace {
  display: grid;
  place-items: center;
  padding: 1.5rem 0.5rem;
  border: 1px dashed var(--line);
  color: var(--dim);
  font: 500 10px/1.6 var(--chrome-face);
  letter-spacing: 0.14em;
  text-align: center;
}

/* An occupied slot: the tenant brings its own chrome, so the vacancy dressing goes. */
.adspace--filled {
  padding: 0;
  border: 0;
}

/* The wall
 *
 * Other people's buttons, in colour. Everything else here is red on black because I drew it;
 * these are the one place the rest of the web gets in looking like itself, and draining them
 * to match would be the opposite of the point.
 *
 * 88x31 is the format but not a guarantee -- a couple in the folder are 176x62 or wider. Each
 * one is held to the 88x31 box and contained, so an odd size letterboxes instead of
 * stretching. */

.wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
  gap: 4px;
}

.wall__btn {
  width: 100%;
  aspect-ratio: 88 / 31;
  object-fit: contain;
}

/* 88x31 is a format you squint at. Hover blows the button (and below, the stamp) up in
   place -- transform moves no neighbors, it just paints the grown one over them. */
.wall__btn,
.stampbook__stamp {
  position: relative;
  transition: transform 120ms ease;
}

.wall__btn:hover,
.stampbook__stamp:hover {
  transform: scale(2);
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  .wall__btn,
  .stampbook__stamp {
    transition: none;
  }
}

/* The wall is the one card with more to show than a column can hold, and the one card whose
   contents are a grid of their own, so it breaks out across all the columns and runs as wide
   as the panel -- which is what a button collection wanted to be all along. Being last, the
   break it forces costs nothing: everything above it balances, then this sits under the lot.
   In the gutter there's a single column to span, so the same rule quietly does nothing. */
.card--wall {
  column-span: all;
  margin-top: 1rem;
}

/* The spanner has to carry its own top margin, because the one above it can't reach: a margin
   that meets a column break is truncated to nothing, so in the packed arrangement the wall
   butts straight against the tallest column. Hence the rule above -- and hence this one, since
   in the gutter there's no break to truncate anything and the two margins would stack instead
   of collapsing (a spanner's don't collapse with what precedes it). One of the two always
   applies and never both. */
.card:has(+ .card--wall) {
  margin-bottom: 0;
}

/* The band is at the foot of the page, which is no place for a nav: down here the strip under
   the masthead is the nav and this card stands down. It comes back with the gutter. */
.card--nav {
  display: none;
}

/* Into the gutter
 *
 * Rail, gap and panel come to 946px, and the page's own padding wants the rest, so this is the
 * first width that fits the pair. The rail takes the second cell and the panel auto-places into
 * the first, and the nav goes with the rail, because up here there's a column beside the
 * reading to hold it. Nothing else changes: the cards go on packing into whatever columns
 * their own width affords, which in a 200px gutter is one. */

@media (min-width: 1120px) {
  .content {
    grid-template-columns: minmax(0, var(--panel-w)) var(--rail-w);
    gap: var(--rail-gap);

    /* The nav strip goes with this rung, and it was what stood between the masthead and the
       panel. Without it the 2rem below the visual is a hole, so the panel comes up to meet it. */
    margin-block: 0 4rem;
  }

  .rail {
    grid-column: 2;
    grid-row: 1;
  }

  .card--nav {
    display: block;
  }

  .nav--head {
    display: none;
  }
}

/* ------------------------------------------------------------------ Panel */

/* No placement of its own: the rail claims its cell and the panel auto-places into whatever
   is left -- the row above the band, or the first column beside the gutter. That's what lets
   the gutter query in the Rail section above hold, since a `grid-column` here would sit later
   in the file at the same specificity and win at every width. The track sizes it, and the
   margins belong to .content. */
.panel {
  position: relative; /* the corner critters perch on its frame */
  width: 100%;
  padding: 1.25rem;

  background: color-mix(in srgb, var(--bg) var(--surface-alpha), transparent);
  border: 1px solid var(--ink);
  border-radius: 0;
}

/* Corner critters
 *
 * The duck (off to /john-duck/) swims at the far end of the About-me title's rule -- the
 * line runs through its base like a waterline, so it hangs a few pixels below the border.
 * The game's MAP icon (off to /club-penguin) is propped the same way on the right end of
 * the footer's top rule. Markup is in pages/home.njk: the duck inside the About title,
 * the map just inside .foot. */

.critter {
  position: absolute;
  z-index: 1;
  line-height: 0;
  transition: transform 0.15s ease;
}

.critter:hover {
  /* not the global a:hover block -- a solid slab behind a cutout reads as a stuck
     background, so these grow a little instead */
  background: none;
  transform: scale(1.12);
}

.critter img {
  width: 100%;
}

#about .section__title {
  position: relative; /* the duck paddles on its bottom rule */
}

.critter--duck {
  right: 0;
  bottom: -5px; /* the title's border is the waterline; this much hull sits under it */
  width: 27px;
}

.critter--map {
  top: -28px; /* 26px wide at 920x1085 is ~31px tall; a few px sit below the rule */
  right: 12px;
  width: 26px;
}

.section + .section {
  margin-top: 2.5rem;
}

.section__title {
  margin-bottom: 0.9rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  font: 600 11px/1 var(--chrome-face);
  letter-spacing: 0.2em;
}

.section p {
  color: var(--text);
  font-size: 0.9rem;
}

.section p + p {
  margin-top: 0.7rem;
}


/* One column until there's width to divide. Every variant below is the same grid with a
   different division of it, so they all wait for the same query. */
.cols {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: 1fr;
}

.box {
  padding: 1rem;
  border: 1px solid var(--line);
}

/* The status light
 *
 * Pinned to the frame's bottom-right corner, and pinned by -1px on both sides so its own border
 * lands exactly on the frame's: the two boxes share that corner's rules rather than one
 * floating inside the other. A whole pixel of daylight between them would read as a sticker.
 *
 * Set in Minecraft, big -- the splash text's face, and the only other place it appears. It's a
 * display face and this is the one label short enough to take one: a single word, which is what
 * makes it a light rather than a readout. Big because a pixel face has no weight axis to lean
 * on; size is the only way it gets bold.
 *
 * No tracking: a pixel face carries its own spacing, and .badge's 0.16em pulls the letters off
 * the grid they were drawn on. Same reason the splash sets it back to normal.
 *
 * A <div> rather than a <p>, which is what it was. `.section p` sets the prose colour and beats
 * a single class, so as a paragraph the badge came out in --text white instead of amber -- and
 * a status light was never a paragraph anyway.
 *
 * It's a badge (see the Badge section) -- the component was built for exactly this and then
 * turned out to be wrong on the rail's card titles, which is where it was first tried. It takes
 * the badge's amber over, because here the colour is the message: --badge-ink is what .badge
 * paints its border, its text and its glow from, so setting that one property on the state
 * turns the whole tag over at once. Green for here, HUD crimson for not. The dot follows on
 * currentColor rather than being told twice.
 *
 * Mine, not the visitors' -- "here now" in the readout card is them. This is what the HUD
 * palette in tokens.css is there for: a status light is the one thing on the page allowed
 * colours that aren't the ink.
 *
 * One rounded corner, and it's the corner facing into the photograph -- the other three stay
 * square because they're the frame's own corner and have to keep its line. It reads as a tab
 * tucked into the picture rather than a box dropped on top of it.
 *
 * The badge's own display and font come from .badge; what's set here is the pinning, the state
 * colour, the face, and the row the dot and the word sit in. */

.status {
  --badge-ink: var(--hud-green);

  position: absolute;
  right: -1px;
  bottom: -1px;
  z-index: 1;

  display: flex;
  align-items: center;
  gap: 0.4rem;

  /* Extra on the left: the rounded corner eats into that side, so an even inset would leave
     the dot sitting closer to the curve than the word does to the square edge opposite. */
  padding: 0.25em 0.5em 0.25em 0.75em;
  border-top-left-radius: 17px;
  font-family: 'Minecraft', var(--chrome-face);
  font-size: 16px;
  letter-spacing: normal;
}

.status[data-status='offline'] {
  --badge-ink: var(--hud-crimson);
}

.status__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 6px currentColor;
}

@media (prefers-reduced-motion: no-preference) {
  .status[data-status='online'] .status__dot {
    animation: pulse 2.4s ease-in-out infinite;
  }
}

@keyframes pulse {
  50% {
    opacity: 0.3;
  }
}

/* The one real photograph on the page, duotoned into the ink so it belongs to the same
   monochrome as everything else. Grayscale, then multiplied over an ink-filled frame: white
   pixels come through as ink, black stays black. A filter chain can't do this --
   sepia/hue-rotate lands on whatever hue the math gives you, not on --ink. */
.portrait-frame {
  position: relative; /* the status light hangs off its corner */
  isolation: isolate; /* keep the blend inside the frame */
  background: var(--ink);
  border: 1px solid var(--line);
}

.portrait {
  display: block;
  width: 100%;
  /*filter: grayscale(1) contrast(1.1);
  mix-blend-mode: multiply;*/
}

/* The knotted mass -- the mission statement's one supporting picture. Unframed, and the
   line it supports IS the caption: prose voice, just pulled back a step so the picture
   leads and the words read as its plate text. */

.knot-fig {
  margin: 1.1rem 0;
}

.knot-fig__img {
  display: block;
  width: 100%;
}

.knot-fig__caption {
  margin-top: 0.45rem;
  font-size: 0.85rem;
  color: color-mix(in oklab, var(--text) 72%, transparent);
}

.knot-fig__plate {
  position: relative; /* the credit pins to the picture's corner, not the figure's */
}

/* The oomoo button's text voice, printed straight onto the plate. Black on purpose in
   both themes: it sits on the artwork, not on the page. */
.knot-fig__credit {
  position: absolute;
  right: 7px;
  bottom: 6px;
  font: 500 7px/1.2 var(--chrome-face);
  letter-spacing: 0.08em;
  color: #000;
}

/* Lists that read like readouts */

.list {
  list-style: none;
}

.list li {
  padding: 0.3rem 0;
  color: var(--text);
  font-size: 0.85rem;
  border-bottom: 1px dotted var(--line);
}

/* The traits grid packs three lists side by side, so its text runs a step smaller than the
   full-width lists elsewhere. */
.cols--traits .list li {
  font-size: 12px;
}

.list li:last-child {
  border-bottom: 0;
}

/* The about facts trade text bullets for tiny gifs. inline-block because the reset blocks
   every img. One square box for all of them -- the gifs come in different shapes (the flag
   is twice as wide as it is tall), and letting each keep its natural width made the row of
   bullets read as three different sizes. contain keeps each one whole inside the box. */
.list__gif {
  display: inline-block;
  width: 20px;
  height: 20px;
  object-fit: contain;
  margin-right: 0.15rem;
  vertical-align: -5px;
}

/* The flag draws edge to edge where the others carry air, so at the same box it reads as
   the biggest of the three. Same 20px box as the rest -- that box is what lines the text
   up in a column -- with padding shrinking the drawn flag to 13x8: a touch squarer than a
   real flag, on purpose, so it holds its own next to the square icons. fill, not contain:
   the file itself is stored square, and contain would draw it 8x8. */
.list__gif--flag {
  padding: 6px 3.5px;
  object-fit: fill;
}

.tag {
  font: 500 10px/1 var(--chrome-face);
  letter-spacing: 0.14em;
  color: var(--dim);
}

/* Kaomoji riding the section titles. The titles' tracking would pull each face apart into
   scattered punctuation, so it resets here; everything else inherits. */
.kao {
  letter-spacing: normal;
}

/* The rolling rows -- the lanyard of identity badges under the About title and the fun-fact
   userboxes under the traits boxes. Server-side each is one .ticker__track of images at
   native height. If the track outgrows the panel, scripts/home/ticker.js clones it and sets
   the pair rolling (--reverse flips the direction); until then -- and for anyone asking for
   less motion, and with JS off -- the row just sits there, hand-scrollable. */
.ticker {
  display: flex;
  overflow-x: auto;
  margin-bottom: 0.9rem;
}

.ticker__track {
  display: flex;
  flex: none;
  align-items: center;
  gap: 4px;
  padding-right: 4px;
}

.ticker--identity .ticker__item {
  height: 15px;
  width: auto;
}

/* Native size: the boxes come drawn at ~240x50 (one at exactly 2x), and smaller than that
   the fine print stops being print. */
.ticker--facts .ticker__item {
  height: 50px;
  width: auto;
}

.ticker--live {
  overflow: hidden;
}

.ticker--live .ticker__track {
  animation: ticker-roll var(--ticker-t, 40s) linear infinite;
}

.ticker--reverse .ticker__track {
  animation-direction: reverse;
}

/* A row you're pointing at is a row you're reading. */
.ticker--live:hover .ticker__track {
  animation-play-state: paused;
}

@keyframes ticker-roll {
  to {
    transform: translateX(-100%);
  }
}

/* The stamp album. Every cell is the classic 99x56 -- the few files that come larger are the
   same stamps at 2x, so cover squeezes them back onto the same perforation without cropping
   anything that matters. */
.stampbook {
  display: grid;
  grid-template-columns: repeat(auto-fill, 99px);
  gap: 6px;
}

.stampbook__stamp {
  width: 99px;
  height: 56px;
  object-fit: cover;
}

@media (min-width: 700px) {
  .panel {
    padding: 2rem 2.5rem;
  }

  .cols {
    grid-template-columns: repeat(var(--cols, 2), 1fr);
  }


  .cols--portrait {
    grid-template-columns: 38% 1fr;
  }

  .cols--feed {
    grid-template-columns: 1fr 34%;
  }
}

/* -------------------------------------------------------------------- Hint */

/* An aside the page only says when you ask: a dotted word in the prose, and a small note over
 * it on hover or focus.
 *
 * No script and no title attribute. `title` would be the one-line version of this, but it's
 * slow to appear, invisible on a touch screen, and unstyleable -- and having both a title and
 * a note would show two tooltips at once. The note is a real element instead, so it reads at
 * whatever size the page reads at and a keyboard can reach it: tabindex on the wrapper is what
 * makes :focus-visible fire, and role="tooltip" is what says the note belongs to the word.
 *
 * display rather than opacity for the hidden state. A hidden absolute box still counts toward
 * the page's scrollable area, and this one is wider than the word it hangs off -- so on a phone
 * an invisible note would be enough to give the whole document a horizontal scrollbar.
 */

.hint {
  position: relative;
  border-bottom: 1px dotted var(--accent);
  cursor: help;
}

.hint__note {
  display: none;
  position: absolute;
  bottom: calc(100% + 0.55rem);
  left: 50%;
  translate: -50% 0;
  z-index: 4;

  width: max-content;
  max-width: min(15rem, 60vw);
  padding: 0.4rem 0.55rem;

  background: var(--bg);
  border: 1px solid var(--accent);
  color: var(--accent);
  font: 500 11px/1.45 var(--chrome-face);
  letter-spacing: 0.02em;
}

.hint:hover .hint__note,
.hint:focus-visible .hint__note {
  display: block;
}

/* ------------------------------------------------------------- Micro-Moos */

.entry {
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
}

.entry + .entry {
  margin-top: 0.75rem;
}

.entry__head {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  align-items: baseline;
}

.entry__title {
  color: var(--ink);
  font-size: 1rem;
  font-weight: 500;
}

.entry__body {
  margin-top: 0.35rem;
  color: var(--text);
  font-size: 0.85rem;
}

/* A moo with a picture attached: a thumbnail in the card, the full thing lives on /moos. */
.entry__moo-img {
  display: block;
  margin-top: 0.6rem;
  max-width: min(16rem, 100%);
  border: 1px solid var(--line);
}

/* The last card in the feed is the door to the rest of the herd. Same box as an entry,
   but it reads in the label voice and lights up whole on hover. */
.entry--more {
  display: block;
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--dim);
  text-decoration: none;
}

.entry--more:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* The sidebar beside the entries -- today the live chat box; a webring widget joins it once
   the site is live. The readout and the ad slot used to live here too and are cards in the
   rail now, which is where the rest of the page's ambient chrome ended up. */

.side + .side {
  margin-top: 1.25rem;
}

.side__title {
  margin-bottom: 0.4rem;
  color: var(--ink);
  font: 500 10px/1 var(--chrome-face);
  letter-spacing: 0.16em;
}

/* The chat box, drawn like every cbox before it: a scrolling log over a one-line form. The
   box fills whatever height its column has -- beside the feed that means the log stretches
   until the form sits level with the feed's last card -- while min-height: 0 down the chain
   keeps the log's own contents from being what sets that height, so the column doesn't
   breathe with every message: the log scrolls inside itself and chat.js keeps it pinned to
   the newest line unless you've scrolled up to read. The 220px floor is for the stacked
   layout, where the column has no height of its own to lend. */

.side--chat {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.shout {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.shout__log {
  flex: 1;
  min-height: 220px;
  overflow-y: auto;
  padding: 0.45rem 0.5rem;
  border: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.shout__note {
  color: var(--dim);
  font-size: 0.75rem;
  font-style: italic;
}

.shout__msg {
  color: var(--text);
  font-size: 0.8rem;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

.shout__who {
  color: var(--ink);
  font: 500 10px/1 var(--chrome-face);
  letter-spacing: 0.1em;
}

.shout__when {
  margin-left: 0.3rem;
  color: var(--dim);
  font: 500 9px/1 var(--chrome-face);
}

.shout__form {
  margin-top: 0.3rem;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.3rem;
}

.shout__name,
.shout__text {
  width: 100%;
  padding: 0.35rem 0.4rem;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--text);
  font: 500 11px/1.4 var(--chrome-face);
}

.shout__name {
  grid-column: 1 / -1;
  color: var(--dim);
}

.shout__name:focus,
.shout__text:focus {
  border-color: var(--ink);
  outline: none;
}

.shout__send {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--line);
  background: none;
  color: var(--ink);
  letter-spacing: 0.14em;
  cursor: pointer;
}

.shout__send:hover {
  background: var(--ink);
  color: var(--bg);
}

/* -------------------------------------------------------------- Favorites */

.favorites__title {
  margin-bottom: 0.9rem;
  color: var(--ink);
  font: 500 12px/1 var(--chrome-face);
  letter-spacing: 0.24em;
}

.fav-grid {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: 1fr;
}

.fav {
  padding: 0.6rem;
  border: 1px dotted var(--line);
}

.fav__label {
  margin-bottom: 0.5rem;
  color: var(--dim);
  font: 500 10px/1 var(--chrome-face);
  letter-spacing: 0.14em;
}

/* The shelf label is a hotlink into the media log filtered to its medium. Same exception
   as the nav: it's chrome, so no underline at rest -- the ↗ carries the "this is a link"
   instead. The hover opts out of the site-wide invert (background would otherwise fall
   through from a:hover): a solid red block behind a 10px label overpowers the whole shelf,
   so it underlines and turns accent rather than inverting. */
a.fav__label {
  display: block;
  text-decoration: none;
}

a.fav__label:hover {
  background: none;
  color: var(--accent);
  text-decoration-line: underline;
}

/* Sized to the label's cap height, sat on the baseline so it reads as punctuation.
   inline-block because the reset blocks every svg, which would drop this to its own line. */
.fav__label-arrow {
  display: inline-block;
  width: 12px;
  height: 12px;
  vertical-align: -2px;
}

.fav__covers {
  display: grid;
  gap: 0.35rem;
  grid-template-columns: repeat(3, 1fr);
}

/* No cover art on hand, so each one is a plate: the title set small over a contour cutout,
   dimmed right down. Reads as artwork at a glance without pretending to be it. */
.cover {
  position: relative;
  display: grid;
  align-items: end;
  aspect-ratio: 2 / 3;
  padding: 0.3rem;
  border: 1px solid var(--line);
  overflow: hidden;
  color: color-mix(in srgb, var(--ink) 92%, transparent);
  font: 500 9px/1.25 var(--chrome-face);
  letter-spacing: 0.02em;
}

.cover::before {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0.28;

  background-color: var(--ink);
  mask-image: var(--cover-src);
  mask-size: 130%;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-image: var(--cover-src);
  -webkit-mask-size: 130%;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
}

.cover span {
  position: relative;
}

/* Real poster art: same 2:3 frame and hairline, but the image fills it edge to edge and
   carries its own title, so the plate's padding and cutout step out of the way.

   It's an <a> to the image file rather than a button, so the shelf still opens a cover with
   the script blocked; scripts/home/lightbox.js intercepts the click and shows it in the
   dialog instead. The pointer cursor and the focus ring both come from the anchor. */
.cover--art {
  display: block;
  width: 100%;
  padding: 0;
}

.cover--art img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* There's no cutout to mask here, so the plate's ::before earns its keep as the hover veil
   instead of being switched off: flat ink over the artwork, which is red in both themes --
   --ink is `red` in the dark theme and #c01a0e in the light one. Flat rather than blended,
   for the reason crosshairs.css gives: a blend mode has to be kept in lockstep with the
   colour, and picking the wrong one makes the whole thing silently vanish.

   This rule has to stay below .cover::before -- same specificity, so order is what settles
   the opacity. */
.cover--art::before {
  opacity: 0;
  mask-image: none;
  -webkit-mask-image: none;
  transition: opacity 120ms ease;
}

.cover--art:hover::before,
.cover--art:focus-visible::before {
  opacity: 0.45;
}

@media (prefers-reduced-motion: reduce) {
  .cover--art::before {
    transition: none;
  }
}

/* Square, and no title in it: the rail's now-playing plate, which borrows the whole recipe
   and puts the meter where the title would go. */
.cover--square {
  aspect-ratio: 1;
}

/* Three shelves across, same width the panel's other columns arrive at. The three covers
   inside a shelf stay three at every width -- they're a row of spines, and one per line would
   read as a list. */
@media (min-width: 700px) {
  .fav-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ------------------------------------------------------- 88x31 buttons */

.btns {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.btn88 {
  display: grid;
  place-items: center;
  width: 88px;
  height: 31px;
  padding: 2px;
  border: 1px solid var(--line);
  color: var(--ink);
  font: 500 7px/1.2 var(--chrome-face);
  letter-spacing: 0.08em;
  text-align: center;
}

.btn88--invert {
  background: var(--ink);
  color: var(--bg);
}

/* ----------------------------------------------------------------- Footer */

/* The band, and the whole of the footer's look: the rule across the top, and the panel's own
   surface behind it edge to edge, so the page finishes on the plane it was written on rather
   than on the bare field.
 *
 * The surface is on the band and not on what's inside it. A box drawn around the credits would
   be a fourth border in a corner of the page that already has three, and the point of the tint
   is the width of it.
 *
 * Padding and no margin, which for a band with a background is the difference between air and a
   hole: margin sits outside the paint, so every bit of it was a stripe the surface didn't reach
   -- above the rule, and again under the blinkies where the page simply stopped. The clear space
   above the footer is .content's own bottom margin and belongs to the content. Everything from
   the rule down is the footer's, and all of it is padding. */
.foot {
  position: relative; /* the map icon props on its top rule */
  padding-block: 1.75rem 2.75rem;
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--bg) var(--surface-alpha), transparent);
}

/* No box of its own, only a width: the credits line up with the content row above rather than
   with the page, which is something the footer was quietly getting wrong. 726px centered sat
   neither under the panel nor under the pair, and was off by half the rail on any screen wide
   enough to show one. Below the stacking point the row is just the panel, so this is too; above
   it, it spans both columns. */
.foot__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  width: 100%;
  max-width: var(--panel-w);
  margin: 0 auto;

  color: var(--dim);
  font: 500 10px/1.8 var(--chrome-face);
  letter-spacing: 0.14em;
}

@media (min-width: 1120px) {
  .foot__inner {
    max-width: calc(var(--cluster-half) * 2);
  }
}

/* Blinkies
 *
 * The little animated bars, last thing on the page -- flex-basis 100% is what takes them a row
 * of their own under the credits and the canary, which share the one above.
 *
 * Held to a common height rather than shown at native size: they come drawn at two scales
 * (150x20 and 300x40), so left alone the row would step up and down. 20px is the smaller pair's
 * own size and exactly half the larger one's, which is the one reduction a pixel face survives
 * without softening. */

.blinkies {
  flex: 1 0 100%;
  margin-top: 1.75rem;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
}

.blinkie {
  width: auto;
  height: 20px;
}

.canary {
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--ink);
  text-align: right;
}
