/* =====================================================================
   dashofblum — the personal hub's own skin, loaded on top of the shared
   site.css it borrows from lightly-salted.com.

   Everything below is a lab notebook: a faint dot grid under the page,
   headings in the same mono as the nav, kickers written as code
   comments, and printer's crop marks on the photographs — the workbench
   and the print shop in one quiet skin. The studio's gold shifts to a
   print-developer rust so this reads as Matthew's own place rather than
   a second Lightly-Salted page.

   Three rules make that safe:

   1. TOKENS GO ON body.site, NOT :root. site.css owns :root; overriding
      there would leak into carshowbingo/, which loads after this file
      and defines its own world. (It touches none of --accent/--paper/
      --ink/--line/--surface — every token there is --csb- prefixed — so
      scoping to body.site keeps the bingo card exactly as it was.)
      The notebook itself goes one step further and skips both bingo
      games outright: every decorative rule is scoped to
      body.site:not(.site--bingo, .site--fairbingo).
   2. SKINS HANG OFF EXISTING CLASSES. Every *skin* selector here hangs
      off a class the hub's page files already emit, so restyling never
      forces a page edit. The one exception is .wall-label at the bottom,
      a component that arrived with its own markup in
      archive-collection-item.php — keep new components down there, and
      keep the skin above honest.
   3. TWO PLACES IN site.css HARDCODE THE GOLD (.btn's `color: #17130a`
      and .btn:hover's `#ecc154`). They can't inherit the new accent, so
      they're restated below. Grep for #ecc154 before adding a component.

   Reset, box-sizing, layout, .reveal and the lightbox all come from
   site.css — not repeated here.
   ===================================================================== */

/* ---- Hub tokens ----------------------------------------------------- */
body.site {
  /* Print-developer rust, off the studio gold.
     --accent-ink on --paper is 8.1:1; --btn-ink on --accent is 5.4:1. */
  --accent:      #c2703d;
  --accent-ink:  #e59a63;
  --accent-hot:  #d98a55;   /* the lift .btn:hover used to get from #ecc154 */
  --btn-ink:     #1a0f07;   /* dark text on the accent fill */

  /* The notebook's two pencil colours. The grid is meant to be barely
     there — much past 8% it stops reading as paper and starts reading
     as a pattern. The marks sit over photographs, so they're brighter. */
  --hub-grid: color-mix(in srgb, var(--ink) 7%, transparent);
  --hub-mark: color-mix(in srgb, var(--ink) 78%, transparent);
}

/* Two columns, not site.css's three: the "Elsewhere" column was retired when the
   Salt Line took over that job, and the grid was left holding an empty third. */
@media (min-width: 720px) { body.site .footer-grid { grid-template-columns: 1.4fr 1fr; } }

/* ---- The page: dot-grid paper --------------------------------------- */
/* On body itself rather than a fixed layer, so it scrolls with the page
   and there's nothing sitting over the text. */
body.site:not(.site--bingo, .site--fairbingo) {
  background-image: radial-gradient(circle, var(--hub-grid) 1px, transparent 1.5px);
  background-size: 24px 24px;
  background-position: center top;
}

/* Bands stay a shade lighter but let the grid through. Their edges are this
   page's own signal trace (partials/signal-trace.php, passed in as
   --hub-trace on <body>) instead of plain rules. The trace is a mask over an
   accent fill, so it takes the page's accent, darkroom red included, and the
   bottom edge is the same capture mirrored. If the variable is missing, the
   fallback mask draws an ordinary 1px rule. */
body.site:not(.site--bingo, .site--fairbingo) .section--alt {
  position: relative;
  background: color-mix(in srgb, var(--paper-2) 80%, transparent);
}
body.site:not(.site--bingo, .site--fairbingo) .section--alt::before,
body.site:not(.site--bingo, .site--fairbingo) .section--alt::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 18px;
  pointer-events: none;
  background: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask: var(--hub-trace, linear-gradient(transparent 8px, #000 8px 9px, transparent 9px)) 0 0 / 600px 18px repeat-x;
  mask: var(--hub-trace, linear-gradient(transparent 8px, #000 8px 9px, transparent 9px)) 0 0 / 600px 18px repeat-x;
}
body.site:not(.site--bingo, .site--fairbingo) .section--alt::before { top: -9px; }
body.site:not(.site--bingo, .site--fairbingo) .section--alt::after  { bottom: -9px; transform: scaleX(-1); }

/* hub.js swaps those still traces for live ones: a <canvas> per edge where
   the signal keeps generating and rolls left, like a scope in roll mode.
   The canvas is taller than the still trace (24px against 18px) to leave
   room for the beam's glow, and centred on the same edge. A band that has
   its canvases gets .has-scope, which retires the still mask. Reduced
   motion, no JS, and print all keep the still trace. */
.hub-scope {
  position: absolute;
  left: 0;
  display: block;
  width: 100%;
  height: 24px;
  pointer-events: none;
}
.hub-scope--top    { top: -12px; }
.hub-scope--bottom { bottom: -12px; }
body.site:not(.site--bingo, .site--fairbingo) .section--alt.has-scope::before,
body.site:not(.site--bingo, .site--fairbingo) .section--alt.has-scope::after { display: none; }
@media print {
  .hub-scope { display: none !important; }
}

/* ---- Type: everything that names something is set in mono ----------- */
/* Plex Mono is self-hosted at 400/500/600 only — 700 would be synthesised. */
body.site:not(.site--bingo, .site--fairbingo) :is(h1, h2, h3, .card__title),
body.site:not(.site--bingo, .site--fairbingo) .brand__type {
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: -.02em;
}

/* Mono runs wide, so the hero headline comes down a step and gets a
   little more measure. */
body.site:not(.site--bingo, .site--fairbingo) .page-hero h1 {
  font-size: clamp(2.1rem, 1.6rem + 2.8vw, 3.8rem);
  max-width: 18ch;
}

/* The home page's name types itself out, the same trick lightly-salted.com's
   hero plays on "A dash of creativity." (site.css .home-hero__quote): clip
   the line to nothing, then step its width open one character at a time
   behind a blinking bar caret. The caret reuses site.css's own hero-caret
   keyframes. hub.js adds .is-typing once fonts are ready, so each 1ch step
   lands on a whole Plex Mono glyph. Without JS, or with reduced motion, the
   name just shows in full.
   --chars comes from index.php, so a renamed record types to its own length.
   Tracking is zeroed because one character is exactly 1ch only in a
   monospace face with no letter-spacing. The extra .1em at the end clears
   the caret's own width, which box-sizing would otherwise take out of the
   last letter. Pace matches the main site's: about 115ms a character. */
body.site--home .page-hero h1.hub-type { letter-spacing: 0; }
body.site--home .page-hero h1.hub-type.is-typing {
  width: 0;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  border-right: .07em solid var(--accent);
  animation:
      hub-type calc(var(--chars, 12) * 115ms) steps(var(--chars, 12), end) .4s forwards,
      hero-caret .8s step-end infinite .4s;
}
@keyframes hub-type { to { width: calc(var(--chars, 12) * 1ch + .1em); } }

/* The brand gets a small rust square, like a status light on a panel. */
body.site:not(.site--bingo, .site--fairbingo) .brand__type {
  display: inline-flex;
  align-items: center;
  gap: .6em;
  font-size: 1rem;
}
body.site:not(.site--bingo, .site--fairbingo) .brand__type::before {
  content: "";
  width: .55em;
  height: .55em;
  background: var(--accent);
}

/* Kickers read as code comments: site.css's rule line becomes a `//`. */
body.site:not(.site--bingo, .site--fairbingo) .kicker::before {
  content: "//";
  width: auto;
  height: auto;
  background: none;
  color: var(--accent);
  letter-spacing: 0;
}

/* site.css's `.section-head p` (0,1,1) outranks `.kicker` (0,1,0), so a
   <p class="kicker"> heading a section ("Lately" on home) would render at
   lede size in muted grey. Put it back to a kicker. */
body.site .section-head .kicker {
  font-size: var(--fs-kicker);
  color: var(--accent-ink);
}

/* "No photo yet" placeholders speak the same mono as everything else. */
body.site:not(.site--bingo, .site--fairbingo) .card__placeholder {
  font-family: var(--font-mono);
  font-size: .9rem;
}

/* ---- Photographs: squared off, with crop marks ---------------------- */
/* Four L-shaped ticks drawn as eight no-repeat gradients, inset from the
   corners the way a print's trim marks sit. The drop-shadow keeps them
   readable over a bright sky. Three places carry them:
   - .card__media (gallery and page cards)
   - .gallery-masonry a, on ::before — site.css already uses ::after
     there for the ⤢ zoom badge
   - figure.framed, only when it has no caption; with one, the figure is
     taller than the photo and the bottom marks would land on the text. */
body.site:not(.site--bingo, .site--fairbingo) .card,
body.site:not(.site--bingo, .site--fairbingo) .gallery-masonry a,
body.site:not(.site--bingo, .site--fairbingo) figure.framed img {
  border-radius: 4px;
}
body.site:not(.site--bingo, .site--fairbingo) .card__media,
body.site:not(.site--bingo, .site--fairbingo) figure.framed {
  position: relative;
}
body.site:not(.site--bingo, .site--fairbingo) .card__media::after,
body.site:not(.site--bingo, .site--fairbingo) .gallery-masonry a::before,
body.site:not(.site--bingo, .site--fairbingo) figure.framed:not(:has(figcaption))::after {
  --tick: 14px;
  --weight: 1.5px;
  content: "";
  position: absolute;
  inset: 10px;
  z-index: 1;
  pointer-events: none;
  background:
      linear-gradient(var(--hub-mark), var(--hub-mark)) left   top    / var(--tick) var(--weight),
      linear-gradient(var(--hub-mark), var(--hub-mark)) left   top    / var(--weight) var(--tick),
      linear-gradient(var(--hub-mark), var(--hub-mark)) right  top    / var(--tick) var(--weight),
      linear-gradient(var(--hub-mark), var(--hub-mark)) right  top    / var(--weight) var(--tick),
      linear-gradient(var(--hub-mark), var(--hub-mark)) left   bottom / var(--tick) var(--weight),
      linear-gradient(var(--hub-mark), var(--hub-mark)) left   bottom / var(--weight) var(--tick),
      linear-gradient(var(--hub-mark), var(--hub-mark)) right  bottom / var(--tick) var(--weight),
      linear-gradient(var(--hub-mark), var(--hub-mark)) right  bottom / var(--weight) var(--tick);
  background-repeat: no-repeat;
  filter: drop-shadow(0 0 1px rgb(0 0 0 / .6));
}

/* The home page's "Lately" cards show the bingo games' 1200×630 link-preview
   images, so their media boxes take that shape instead of site.css's 4:3.
   At 4:3 the crop cut each preview's title and bingo card in half. The
   gallery photo beside them just crops wider. */
body.site--home .card__media { aspect-ratio: 40 / 21; }

/* ---- Buttons: the two hardcoded golds, restated --------------------- */
body.site .btn { color: var(--btn-ink); }
body.site .btn:hover { background: var(--accent-hot); border-color: var(--accent-hot); }

/* ---- Chips: inert in site.css, given a hover here ------------------- */
body.site .chip {
  transition: color var(--speed), border-color var(--speed), background var(--speed);
}
body.site .chip:hover {
  color: var(--accent-ink);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

/* ---- Darkroom mode -------------------------------------------------- */
/* Type "darkroom" (hub.js). Kills the white light, drops a red safelight
   over everything and pushes the photographs through the developer.
   The notebook has no lighting or grain of its own, so this mode brings
   both — those layers exist only while it's on. */
html.hub-darkroom body.site {
  --accent:      #c4342a;
  --accent-ink:  #ff8f79;
  --accent-hot:  #e04b3c;
  --hub-grid:    color-mix(in srgb, #ff8f79 9%, transparent);
  --hub-mark:    #ff8f79;
}
html.hub-darkroom body.site::before,
html.hub-darkroom body.site::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
}
/* The safelight and vignette, behind the content. */
html.hub-darkroom body.site::before {
  z-index: -1;
  background:
      radial-gradient(120% 100% at 50% 34%,
          transparent 26%,
          color-mix(in srgb, #070304 92%, transparent) 100%),
      radial-gradient(60rem 46rem at 50% -10%,
          color-mix(in srgb, #c4342a 34%, transparent),
          transparent 72%);
}
/* The grain, over everything including photographs. Dialogs live in the
   top layer, so the lightbox still opens above it. */
html.hub-darkroom body.site::after {
  z-index: 9999;
  opacity: .16;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}
html.hub-darkroom body.site .card__media img,
html.hub-darkroom body.site figure.framed img,
html.hub-darkroom body.site .gallery-masonry img {
  filter: sepia(1) hue-rotate(-42deg) saturate(3.2) brightness(.72);
}

/* The mode's only piece of injected markup (hub.js builds it). */
.hub-darkroom__badge {
  position: fixed;
  z-index: 10000;
  right: 1rem;
  bottom: 1rem;
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: #ff8f79;
  background: rgba(10, 3, 4, .78);
  border: 1px solid rgba(255, 143, 121, .35);
  border-radius: 999px;
  padding: .45em 1em;
  pointer-events: none;
}

/* ---- The wall label -------------------------------------------------- */
/* A museum placard for archive-collection-item.php: maker, dates and spec
   rows ruled off from each other, with the interpretive prose kept below.
   This is the file's one piece of component CSS — see rule 2 up top. */
.wall-label {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) 1fr;
  max-width: 68ch;              /* matches .prose, so label and text align */
  margin: 0;
  border-top: 1px solid var(--line);
}
.wall-label > dt,
.wall-label > dd {
  margin: 0;
  padding: .7rem 0;
  border-bottom: 1px solid var(--line);
}
.wall-label > dt {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
  padding-right: 1.5rem;
  align-self: baseline;
}
.wall-label > dd { color: var(--ink); }

/* Stacked: the label sits on its own line above its value, and only the
   value row carries the rule so the pair still reads as one entry. */
@media (max-width: 560px) {
  .wall-label { grid-template-columns: 1fr; }
  .wall-label > dt { padding: .7rem 0 .1rem; border-bottom: 0; }
  .wall-label > dd { padding-top: 0; }
}

/* ---- The jump menu (hub.js) ------------------------------------------ */
/* The header's "/" button. margin-left:auto parks it beside the nav on
   desktop and beside the menu toggle on phones, without upsetting
   site.css's space-between. It ships [hidden]; hub.js reveals it. */
.hub-jump-btn {
  margin-left: auto;
  margin-right: clamp(.2rem, 1.2vw, .9rem);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.1rem;
  height: 2.1rem;
  padding: 0 .55rem;
  font-family: var(--font-mono);
  font-size: .9rem;
  line-height: 1;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 4px;
  cursor: pointer;
  transition: color var(--speed), border-color var(--speed);
}
.hub-jump-btn:hover,
.hub-jump-btn:focus-visible {
  color: var(--accent-ink);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
.hub-jump-btn[hidden] { display: none; }

/* site.css zeroes every margin, which would pin a modal dialog to the top
   left; the margin here is what places it. */
.hub-jump {
  width: min(40rem, calc(100vw - 2rem));
  margin: 12vh auto auto;
  padding: 0;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: var(--shadow-lg);
  font-family: var(--font-mono);
}
.hub-jump::backdrop { background: rgb(8 7 10 / .62); }
.hub-jump__bar {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .85rem 1rem;
  border-bottom: 1px solid var(--line);
}
.hub-jump__prompt { color: var(--accent); font-weight: 600; }
.hub-jump__input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: transparent;
  border: 0;
  outline: 0;
  caret-color: var(--accent);
}
.hub-jump__input::placeholder { color: var(--muted); opacity: .75; }
.hub-jump__esc {
  font: inherit;
  font-size: .68rem;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: .12rem .38rem;
}
.hub-jump__list {
  list-style: none;
  padding: .4rem;
  max-height: min(26rem, 60vh);
  overflow-y: auto;
}
.hub-jump__item a {
  display: grid;
  grid-template-columns: 6.5rem 1fr;
  gap: .05rem 1rem;
  padding: .55rem .7rem;
  border-radius: 3px;
  color: inherit;
}
.hub-jump__item[aria-selected="true"] a { background: color-mix(in srgb, var(--accent) 15%, transparent); }
.hub-jump__kind {
  grid-row: span 2;
  align-self: center;
  font-size: .64rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.hub-jump__title { font-size: .95rem; line-height: 1.3; }
.hub-jump__meta { font-size: .74rem; color: var(--muted); line-height: 1.3; }
.hub-jump__empty { padding: .8rem .7rem; font-size: .85rem; color: var(--muted); }
@media (max-width: 480px) {
  .hub-jump__item a { grid-template-columns: 1fr; }
  .hub-jump__kind { grid-row: auto; }
}

/* ---- Contact-sheet strips (gallery-render.php, $contactSheet) -------- */
/* Under each gallery photo, the line a contact sheet carries: frame number,
   the camera's file number, the published size. The strip sits inside the
   link, at a fixed height, so the crop marks and the zoom badge can be lifted
   by exactly that much and stay on the photograph. It never wraps; on a
   narrow phone the size drops off the end first. */
.gallery-masonry a.has-sheet { --sheet-h: 2.1rem; }
.gallery-sheet {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  height: var(--sheet-h);
  padding: 0 .8rem;
  overflow: hidden;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--dark);
  border-top: 1px solid var(--line);
}
.gallery-sheet__frame { color: var(--accent-ink); }
.gallery-masonry a.has-sheet::after { bottom: calc(.5rem + var(--sheet-h)); }
body.site:not(.site--bingo, .site--fairbingo) .gallery-masonry a.has-sheet::before { bottom: calc(10px + var(--sheet-h)); }

/* ---- Boot screen (archive-collection-item.php, computers) ------------ */
/* The machine's own start-up text, built from its wall label. Amber
   phosphor in the terminal face, faint scanlines, lines arriving one after
   another, and a block cursor after READY. */
.boot-screen {
  margin-top: clamp(1.2rem, 3vw, 2rem);
  max-width: 44rem;
  padding: 1rem 1.25rem 1.15rem;
  font-family: var(--font-terminal);
  font-size: clamp(1.15rem, 1rem + .6vw, 1.45rem);
  line-height: 1.3;
  color: var(--accent-ink);
  background:
      repeating-linear-gradient(transparent 0 3px, rgb(0 0 0 / .24) 3px 4px),
      #0b0a0d;
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: inset 0 0 2.5rem rgb(0 0 0 / .65);
  text-shadow: 0 0 6px color-mix(in srgb, var(--accent) 45%, transparent);
}
.boot-screen > span {
  display: block;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  opacity: 0;
  animation: hub-boot-line .01s linear forwards;
  animation-delay: calc(var(--i, 0) * 170ms + 250ms);
}
@keyframes hub-boot-line { to { opacity: 1; } }
.boot-screen__ready::after {
  content: "";
  display: inline-block;
  width: .55em;
  height: .95em;
  margin-left: .3em;
  vertical-align: -.1em;
  background: currentColor;
  animation: hub-boot-cursor 1s step-end infinite;
}
@keyframes hub-boot-cursor { 50% { opacity: 0; } }

/* ---- Decode mode (hub.js, type "decode") ----------------------------- */
/* A small terminal pinned bottom-left, opposite the darkroom badge, reading
   the bottom channel of whichever band trace is on screen. The workbench
   uses the same readout inline, under its scope (--inline). The trace being
   read burns a little brighter. */
.hub-decode {
  position: fixed;
  z-index: 10000;
  left: 1rem;
  bottom: 1rem;
  width: min(22rem, calc(100vw - 2rem));
  padding: .7rem .9rem .8rem;
  font-family: var(--font-mono);
  font-size: .74rem;
  line-height: 1.55;
  letter-spacing: .03em;
  color: var(--ink);
  background: rgba(10, 9, 12, .92);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 4px;
  box-shadow: var(--shadow);
  pointer-events: none;
}
.hub-decode__head {
  margin-bottom: .35rem;
  font-size: .64rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.hub-decode__bytes { list-style: none; padding: 0; color: var(--muted); white-space: pre; }
.hub-decode__bits { min-height: 1.55em; color: var(--ink); white-space: pre; }
.hub-decode__rx {
  margin-top: .3rem;
  padding-top: .35rem;
  border-top: 1px solid var(--line);
  color: var(--accent-ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.hub-decode--inline {
  position: static;
  width: auto;
  margin-top: .9rem;
  background: #0b0a0d;
  box-shadow: none;
}
.hub-scope.is-decoding { filter: brightness(1.45); }

/* ---- The workbench (workbench.php) ------------------------------------ */
.workbench-legend dd { display: flex; align-items: center; gap: .9rem; }
.workbench-glyph { flex: none; width: 48px; height: 16px; color: var(--accent-ink); }
/* Marginalia under the legend, in the instrument's voice rather than the
   page's. It replaced two paragraphs of prose: the page is a secret, and a
   secret shouldn't read like an article. */
.workbench-note {
  margin-top: 1.4rem;
  max-width: 64ch;
  font-family: var(--font-mono);
  font-size: .76rem;
  line-height: 1.85;
  letter-spacing: .04em;
  color: var(--muted);
}
.workbench__label {
  display: block;
  margin-bottom: .4rem;
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}
.workbench__input {
  width: 100%;
  padding: .7rem .85rem;
  font-family: var(--font-mono);
  font-size: 1rem;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 4px;
  caret-color: var(--accent);
}
.workbench__input:focus { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.workbench-scope {
  display: block;
  width: 100%;
  height: 180px;
  margin-top: 1rem;
  background: #0b0a0d;
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: inset 0 0 2.5rem rgb(0 0 0 / .6);
}
.workbench-table-wrap { margin-top: 1rem; overflow-x: auto; }
.workbench-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: .8rem; }
.workbench-table th,
.workbench-table td { padding: .35rem .6rem; text-align: left; border-bottom: 1px solid var(--line); }
.workbench-table th { font-size: .64rem; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.workbench-table td:first-child { color: var(--accent-ink); }

/* ---- X-Y mode (assets/js/xy.js) --------------------------------------- */
/* The scope's other mode. A scope in X-Y lets the two channels deflect the
   beam instead of plotting against time, which is how a vector arcade monitor
   worked — so in X-Y this one plays asteroids. The transmitter's furniture
   goes away, the screen squares up to a 4:3 tube, and the canvas draws a game.
   None of it is reachable without JS, which is why xy.js builds the HUD and
   the thumb pad rather than workbench.php shipping markup that does nothing. */
.workbench.is-xy .workbench__label,
.workbench.is-xy .workbench__input,
.workbench.is-xy .hub-decode--inline,
.workbench.is-xy .workbench-table-wrap { display: none; }

/* max-*width*, not max-height: aspect-ratio is doing the sizing here, and a
   height cap would fight the ratio instead of bounding the box. */
.workbench-scope.is-xy {
  height: auto;
  aspect-ratio: 4 / 3;
  max-width: min(100%, calc(70vh * 4 / 3));
  margin-inline: auto;
  touch-action: none;
}
.workbench-scope.is-xy:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }

/* The score lives on the tube, in vector type, the way it always did. This
   line is underneath it so the numbers are also real text, and so the way
   out is a real button. */
.workbench-hud {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem 1rem;
  margin-top: .9rem;
  font-family: var(--font-mono);
  font-size: .74rem;
  letter-spacing: .08em;
}
.workbench-hud__stat { margin: 0; color: var(--accent-ink); }
.workbench-hud__exit {
  padding: .35rem .7rem;
  font: inherit;
  letter-spacing: .1em;
  color: var(--muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  cursor: pointer;
}
.workbench-hud__exit:hover {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}

/* The thumb pad only turns up where there's a thumb. */
.workbench-pad { display: none; }
@media (hover: none) and (pointer: coarse) {
  .workbench-pad {
    display: flex;
    gap: .5rem;
    margin-top: .8rem;
    touch-action: none;
  }
  .workbench-pad button {
    flex: 1;
    min-height: 3.4rem;
    font-size: 1.2rem;
    line-height: 1;
    color: var(--accent-ink);
    background: color-mix(in srgb, var(--accent) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
    border-radius: 6px;
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
  }
  .workbench-pad button:active { background: color-mix(in srgb, var(--accent) 22%, transparent); }
}

/* ---- Print ------------------------------------------------------------ */
/* Pages print as clean lab sheets: white paper, dark type, crop marks and
   the wall label kept, the screen furniture gone. */
@media print {
  @page { margin: 14mm; }
  body.site {
    --paper: #fff; --paper-2: #fff; --surface: #fff; --dark: #fff;
    --ink: #111; --muted: #555; --line: #c9c9c9;
    --accent: #8a4a22; --accent-ink: #8a4a22;
    --hub-mark: #111;
    background: #fff !important;
    color: #111;
  }
  .site-header, .site-footer, .skip-link, .hub-jump, .hub-darkroom__badge,
  .link-back, .link-arrow, .ls-net,
  .workbench-hud, .workbench-pad { display: none !important; }
  html.hub-darkroom body.site::before,
  html.hub-darkroom body.site::after { display: none !important; }
  /* Anything still waiting for a scroll to fade in has to print anyway. */
  .js .reveal { opacity: 1 !important; transform: none !important; }
  .section, .section--tight, .page-hero { padding-block: 0 1.2rem !important; }
  .section--alt::before, .section--alt::after { display: none !important; }
  main::before {
    content: "Matthew Blum \00B7  dashofblum.lightly-salted.com";
    display: block;
    margin-bottom: 1.2rem;
    padding-bottom: .4rem;
    border-bottom: 1px solid #111;
    font-family: var(--font-mono);
    font-size: 9pt;
    letter-spacing: .08em;
  }
  body.site--home .page-hero h1.hub-type.is-typing { width: auto !important; border: 0 !important; animation: none !important; }
  .card, .gallery-masonry a, figure.framed, .wall-label, .boot-screen { break-inside: avoid; box-shadow: none !important; }
  .card:hover { transform: none; }
  .gallery-masonry { max-width: none; gap: 6mm; }
  .card__media::after, .gallery-masonry a::before, figure.framed::after { filter: none !important; }
  .gallery-masonry a::after { display: none !important; }
  .boot-screen { background: #fff; text-shadow: none; }
  .boot-screen > span { opacity: 1; animation: none; }
  .boot-screen__ready::after { animation: none; }
  .prose a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .85em; color: #555; overflow-wrap: anywhere; }
}

/* ---- Reduced motion -------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .boot-screen > span { opacity: 1; animation: none; }
  .boot-screen__ready::after { animation: none; }
  /* Mirrors site.css's guard on .home-hero__quote. hub.js already skips the
     class under reduced motion; this covers it being added anyway. */
  body.site--home .page-hero h1.hub-type.is-typing { width: auto; border: 0; white-space: normal; animation: none; }
  body.site .card:hover { transform: none; }
}
