/* The blog's own styles, on top of ulex.

   ulex (public/ulex) is the foundation: tokens, type, components, and the
   contrast it verifies. This file is the blog's look and can use any CSS --
   it is not held to ulex's closed vocabulary. Reach for ulex tokens
   (--ulex-*) for colour and spacing anyway, so dark/light and contrast stay
   in step with the theme. Unlayered, so it wins over ulex's @layer rules. */

/* ---------- fonts ---------- */

/* The Zensical site's fonts, self-hosted (imported in Base.astro), for the
   whole page, header included. ulex reads these tokens wherever it sets a
   font, so code picks up the mono and metadata -- which quiet sets in mono
   -- takes the text face. Two attributes of specificity, so this beats the
   theme's own [data-ulex-theme="quiet"] block whatever the load order. */
:root[data-ulex-theme] {
  --ulex-font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
  --ulex-font-mono: "JetBrains Mono Variable", ui-monospace, Menlo, Consolas, monospace;
  --ulex-font-meta: var(--ulex-font-sans);
}
body {
  font-family: var(--ulex-font-sans);
}
/* ulex sets the mono face on <pre> and skips `pre > code` in its inline code
   rule, which leaves the browser's own `code { font-family: monospace }` in
   charge inside every code block -- so blocks showed the generic system
   monospace, never the mono token. Inherit the <pre>'s face instead.
   Reported upstream (mikegsaunders/ulex#9). */
pre > code {
  font-family: inherit;
}

/* ---------- reading column ---------- */

/* The text column's width; the header band is derived from it. 45rem is
   ~72ch of Inter at 16px. */
:root {
  --blog-column: 45rem;
}

/* quiet is tuned for app screens (13px body, 15px h1). Inside the column,
   restore ulex's default reading scale -- the values from its
   01-contract.css -- so posts read at 16px while the header keeps quiet's
   density. Centred at the measure, which is in ch and so resolves against
   the restored size (about 600px). Holds everything in <main>, so title,
   body, pager and footer share one width. */
[data-blog-column] {
  --ulex-text-1: clamp(1.75rem, 1.5rem + 1.1vw, 2.25rem);
  --ulex-text-2: clamp(1.5rem, 1.3rem + 0.8vw, 1.875rem);
  --ulex-text-3: clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem);
  --ulex-text-4: 1.125rem;
  --ulex-text-5: 1rem;
  --ulex-text-6: 0.9375rem;
  --ulex-text-7: 0.875rem;
  --ulex-text-8: 0.8125rem;
  /* A little wider than quiet's 62ch, as the Zensical site was. Set here so
     prose inside, which also reads the measure, follows. */
  --ulex-measure: var(--blog-column);
  font-size: var(--ulex-text-5);
  inline-size: 100%;
  max-inline-size: var(--ulex-measure);
  margin-inline: auto;
}

/* ---------- dark palette ---------- */

/* Zensical's dark ("slate") palette rather than quiet's: a near-black page
   with white headings over softer body text. Values are Zensical's own,
   flattened from its translucent text (hsla 225 15% 90% at 82% and 56%) onto
   its background. Contrast on #0b0c0f: body 10.4, headings 19.6, secondary
   5.3 -- all AA. A darker background only raises the contrast of every other
   quiet token, so nothing drops below AA. */
[data-ulex-theme="quiet"][data-ulex-mode="dark"] {
  --ulex-bg: #0b0c0f;
  --ulex-surface: #14171f;
  --ulex-surface-hi: #1c2029;
}
[data-ulex-mode="dark"] [data-blog-column] {
  --ulex-text: #bbbdc2;
  --ulex-text-subtle: #838589;
  color: var(--ulex-text);
}
[data-ulex-mode="dark"] [data-blog-column] :is(h1, h2, h3, h4) {
  color: #ffffff;
}

/* ---------- palette: Spacedust (dark) ---------- */

/* The Spacedust terminal theme (as in WezTerm; colours from
   iTerm2-Color-Schemes), switched on by data-blog-palette="spacedust" on
   <html> in Base.astro; remove the attribute to return to the slate palette
   above. Dark mode only. Roles mirror the slate structure: bright white
   headings (17.0:1 on the page), the foreground at 82% for body text (10.1,
   near Zensical's 10.4), bright cyan for secondary text (6.7) and cyan for
   links and accent (6.5). Code blocks and collapsibles are raised on
   #0e2a33, where every code colour still passes AA (5.4 or better). The
   earlier Solarized experiment is in git history (23f26cc). */
:root[data-blog-palette="spacedust"][data-ulex-mode="dark"] {
  --ulex-bg: #0a1e24;            /* background */
  --ulex-surface: #0e2a33;       /* raised panels */
  --ulex-surface-hi: #133540;    /* hover */
  --ulex-text: #c3caa5;          /* foreground at 82% */
  --ulex-text-dim: #c3caa5;
  --ulex-text-subtle: #83a7b4;   /* bright cyan */
  --ulex-accent: #06afc7;        /* cyan */
  --ulex-ring: #06afc7;
  --ulex-edge-subtle: #173a45;
  --ulex-edge-control: #708284;  /* cursor grey, 4.3:1 for boundaries */
  --ulex-edge-strong: #83a7b4;
  --ulex-tone-accent: #06afc7;   /* cyan */
  --ulex-tone-info: #67a0ce;     /* bright blue */
  --ulex-tone-success: #5cab96;  /* green */
  --ulex-tone-warning: #e3cd7b;  /* yellow */
  --ulex-tone-danger: #ff8a3a;   /* bright red (orange) */
}
:root[data-blog-palette="spacedust"][data-ulex-mode="dark"] [data-blog-column] {
  --ulex-text: #c3caa5;
  --ulex-text-subtle: #83a7b4;
}
:root[data-blog-palette="spacedust"][data-ulex-mode="dark"] [data-blog-column] :is(h1, h2, h3, h4) {
  color: #fefff1;                /* bright white */
}

/* ---------- header ---------- */

/* Sticky, translucent and blurred, as the old Zensical site's header was.
   Where backdrop-filter is unsupported the background stays opaque, so text
   behind it can never show through unblurred. */
[data-ulex-nav] {
  position: sticky;
  inset-block-start: 0;
  z-index: 10;
  box-shadow: none;
  background: var(--ulex-bg);
  /* The header's contents sit in a band halfway between the window edges
     and the text column -- a quarter of the leftover width each side, not
     half -- so the title is neither on the edge nor stacked directly over
     the text. At 1905px that puts it ~296px in, where machinelibrary's is.
     On narrow screens it falls back to ulex's 16px. */
  padding-inline: max(var(--ulex-space-4), calc((100% - var(--blog-column)) / 4));
}
/* The page's own colour rather than a band of its own, so at the top of a
   page the header sits straight on the page; the hairline below it only
   appears once content scrolls underneath (data-at-top is set by script in
   Base.astro while the page is at the top, so without script the line just
   always shows). Where backdrop-filter is unsupported this stays opaque. */
[data-ulex-nav][data-ready] {
  transition: border-color 150ms ease;
}
[data-ulex-nav][data-at-top] {
  border-block-end-color: transparent;
}

/* The last link's own padding would stop it short of the band's edge. */
[data-ulex-nav] a:last-child {
  padding-inline-end: 0;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  [data-ulex-nav] {
    /* machinelibrary.pages.dev's values: a lighter 8px blur and no
       saturation boost, so text passing underneath softens rather than
       smears. */
    background: color-mix(in srgb, var(--ulex-bg) 70%, transparent);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
}

/* Plain text links, as Zensical's header has, instead of ulex's button-like
   treatment (hover fill, current-page fill and accent underline). The
   current page is marked by full-strength text and weight -- not colour
   alone -- and by aria-current for assistive technology. */
[data-ulex-nav] a,
[data-ulex-nav] a:hover,
[data-ulex-nav] a[aria-current] {
  background: none;
  box-shadow: none;
}
[data-ulex-nav] a {
  padding-inline: var(--ulex-space-2);
  transition: color var(--ulex-transition-fast);
}
[data-ulex-nav] a:hover,
[data-ulex-nav] a[aria-current] {
  color: var(--ulex-text);
}
[data-ulex-nav] a[aria-current] {
  font-weight: var(--ulex-weight-semibold);
}
[data-ulex-nav] .site-title-emoji {
  display: inline-flex;
  gap: 0.15em;
  margin-inline-start: 0.4em;
}
[data-ulex-nav] .site-title-emoji img {
  inline-size: 1.1em;
  block-size: 1.1em;
}
[data-ulex-nav] .site-title {
  padding-inline-start: 0;
  color: var(--ulex-text);
  font-size: 1.125rem;
  font-weight: var(--ulex-weight-semibold);
}

/* The page title that joins the site title once the h1 scrolls away
   (script in Base.astro). It takes the header's spare width with a zero
   basis, so a long title truncates with an ellipsis instead of wrapping the
   header onto a second line. Hidden but still laid out, so nothing shifts
   when it appears. */
.header-page-title {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  gap: 0.6em;
  min-inline-size: 0;
  color: var(--ulex-text);
  font-size: var(--ulex-text-3);
  font-weight: var(--ulex-weight-medium, 500);
  opacity: 0;
  translate: 0 0.25rem;
  transition: opacity 180ms ease, translate 180ms ease;
  pointer-events: none;
}
[data-ulex-nav][data-show-title] .header-page-title {
  opacity: 1;
  translate: 0 0;
}
.header-page-title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
  .header-page-title {
    translate: none;
    transition: opacity 180ms ease;
  }
}

/* Headings reached by an anchor land below the sticky header, not under it. */
[data-ulex-main] :is(h1, h2, h3, h4, [id]) {
  scroll-margin-block-start: calc(var(--ulex-nav-h) + var(--ulex-space-4));
}

/* ---------- post list ---------- */

/* Date first, then a large title, then a dimmed summary; posts separated by
   a short rule rather than full-width lines. The date is moved up with
   `order` so the source keeps the title first -- someone jumping between
   headings lands on the title, then hears its date. */
.post-list {
  gap: var(--ulex-space-6);
}
.post-list > article {
  gap: 0.4rem;
}
.post-list > article > p[data-ulex-emphasis] {
  order: -1;
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ulex-text-subtle);
}
.post-list h2 {
  font-size: 1.5rem;
  line-height: 1.25;
}
.post-list > article > p:last-child:not([data-ulex-emphasis]) {
  color: var(--ulex-text-subtle);
}
.post-list > article + article::before {
  content: "";
  order: -2;
  display: block;
  inline-size: 2.5rem;
  block-size: 1px;
  margin-block-end: 1.1rem;
  background: var(--ulex-edge-control);
}

/* Titles read as headings, not links; the underline comes back on hover
   and focus so they still show they are links. */
.post-list h2 a {
  /* Inherit the heading's colour (white in dark mode) rather than the body
     text colour, which made titles and summaries nearly the same tone. */
  color: inherit;
  text-decoration: none;
}
.post-list h2 a:is(:hover, :focus-visible) {
  text-decoration: underline;
}

/* ---------- page to page ---------- */

/* Cross-document view transitions: the new page cross-fades in, and the
   header -- named, so it is matched across pages -- stays where it is rather
   than blinking as the page reloads. Browsers without support just navigate.
   Skipped for anyone who asks for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}
[data-ulex-nav] {
  view-transition-name: site-header;
}
/* No cross-fade for the header itself: coming from a scrolled page, the old
   header's line would fade out over the transition -- a visible flash. The
   new header simply replaces it. */
::view-transition-old(site-header) {
  display: none;
}
::view-transition-new(site-header) {
  animation: none;
}

/* ---------- code blocks ---------- */

/* ulex-docs.js adds a Copy button to every code block, with no attribute to
   opt out, and at its 24px target size it is taller than a one-line block.
   The blog's snippets are short, so hide it; the code is still selectable,
   and the <pre> stays keyboard-scrollable. Tracked upstream as a ulex issue. */
[data-ulex-code-copy] {
  display: none;
}

/* ---------- on this page ---------- */

/* Two copies from Toc.astro, one shown per width: a collapsed list at the
   top of the post by default, and the sticky ulex contents list beside the
   text once there is room for it outside the centred column. */
.toc-aside {
  display: none;
}
.toc-inline ul {
  margin: 0;
  padding-inline-start: 0;
  list-style: none;
  font-size: var(--ulex-text-7);
}
.toc-inline ul ul {
  padding-inline-start: var(--ulex-space-4);
}
.toc-inline a {
  display: block;
  padding-block: var(--ulex-space-1);
  text-decoration: none;
}
/* Long headings, or one long unbroken word like "a/b/c", wrap rather than
   being clipped by the list's scroll box. */
.toc-aside a,
.toc-inline a {
  overflow-wrap: anywhere;
}
.toc-aside p {
  margin: 0 0 var(--ulex-space-2);
  padding-inline-start: var(--ulex-space-2);
  color: var(--ulex-text);
  font-size: var(--ulex-text-8);
  font-weight: var(--ulex-weight-semibold);
}

/* Beside the text from 66rem (1056px) -- which takes in a 1920px screen at
   Windows' default 150% scaling, a ~1263px viewport once the scrollbar is
   counted, that an earlier 80rem breakpoint missed. The column and the list
   and the column moves left only as far as the list needs, so on narrower
   screens the pair is off-centre rather than the list missing. */
@media (min-width: 66rem) {
  /* Shift only by what the list is short of: once the space right of a
     centred column holds it (from about 77rem), the shift is zero and the
     column stays under the header band. */
  [data-blog-column]:has(> .post.has-toc) {
    --toc-inline-size: 14rem;
    --toc-gap: 2rem;
    position: relative;
    inset-inline-start: calc(
      -1 * max(0px, var(--toc-inline-size) + var(--toc-gap) - (100% - var(--blog-column)) / 2)
    );
  }
  .post.has-toc {
    position: relative;
  }
  .toc-inline {
    display: none;
  }
  /* Out of flow, to the right of the column, spanning the post's height so
     the sticky list inside can follow the reader down. */
  .toc-aside {
    display: block;
    position: absolute;
    inset-block: 0;
    inset-inline-start: calc(100% + var(--toc-gap));
    inline-size: var(--toc-inline-size);
  }
}

/* ---------- image zoom ---------- */

/* Post images larger than the column link to a bigger copy, which the
   script in Base.astro shows in this dialog. */
a.zoom {
  display: block;
  cursor: zoom-in;
}
a.zoom:focus-visible {
  outline: 2px solid var(--ulex-ring);
  outline-offset: 2px;
}
dialog.lightbox {
  max-inline-size: 100vw;
  max-block-size: 100dvh;
  inline-size: 100vw;
  block-size: 100dvh;
  margin: 0;
  padding: var(--ulex-space-4);
  border: 0;
  background: transparent;
  box-shadow: none;
  cursor: zoom-out;
}
dialog.lightbox[open] {
  display: grid;
  place-items: center;
}
dialog.lightbox::backdrop {
  background: color-mix(in srgb, #000 85%, transparent);
}
dialog.lightbox img {
  max-inline-size: 100%;
  max-block-size: calc(100dvh - 2 * var(--ulex-space-4));
  object-fit: contain;
}
dialog.lightbox form {
  position: fixed;
  inset-block-start: var(--ulex-space-4);
  inset-inline-end: var(--ulex-space-4);
  margin: 0;
}

/* ---------- headings in posts ---------- */

/* Zensical's spacing: 1.6em above h2 and h3, less below, so a heading
   belongs to the text after it. ulex prose spaces every block the same
   (--ulex-space-4), which runs headings into the paragraph before. The block
   after a heading drops its own top margin so the heading's bottom margin is
   the gap. A heading that opens the post keeps ulex's zero top margin. */
[data-blog-column] [data-ulex-prose] > h2 {
  margin-block: 1.6em 0.64em;
}
[data-blog-column] [data-ulex-prose] > h3 {
  margin-block: 1.6em 0.8em;
}
[data-blog-column] [data-ulex-prose] > h4 {
  margin-block: 1.25em 0.8em;
}
[data-blog-column] [data-ulex-prose] > :is(h2, h3, h4) + * {
  margin-block-start: 0;
}
[data-blog-column] [data-ulex-prose] > :is(h2, h3, h4):first-child {
  margin-block-start: 0;
}

/* ---------- page end ---------- */

/* Room below the last thing on the page (usually the pager), plus the
   device's safe area, so a browser toolbar that floats over the page --
   as current iOS Safari's does -- cannot sit on top of it. */
[data-ulex-main] {
  padding-block-end: calc(var(--ulex-space-12) + env(safe-area-inset-bottom, 0px));
}

/* ---------- space under the header ---------- */

/* Room between the header and the first heading -- 56px, near
   machinelibrary's -- instead of ulex's 24px, which read as cramped. */
[data-ulex-main] {
  padding-block-start: calc(var(--ulex-space-12) + var(--ulex-space-2));
}

/* ---------- collapsible sections ---------- */

/* A chevron at the far end of the summary, as Zensical has, in place of
   ulex's small triangle before the text: pointing right when closed, down
   when open. ulex's own ::before marker is reused (moved with `order` and
   pushed right with an auto margin), so its rotate transition still applies.
   Proposed upstream as mikegsaunders/ulex#10. */
details > summary::before {
  order: 1;
  margin-inline-start: auto;
  inline-size: 0.45em;
  block-size: 0.45em;
  border: 0;
  border-inline-end: 1.5px solid currentColor;
  border-block-end: 1.5px solid currentColor;
  rotate: -45deg;
}
details[open] > summary::before {
  rotate: 45deg;
}

/* Inside a collapsible the content is a ulex stack with an even gap, so
   give headings the same "more above than below" rhythm as the post body,
   and drop ulex's own margin on code blocks, which doubled the gap above
   them. */
details[data-ulex-admonition] > [data-ulex-stack] > :is(h3, h4):not(:first-child) {
  margin-block-start: var(--ulex-space-3);
}
details[data-ulex-admonition] > [data-ulex-stack] > [data-ulex-code] {
  margin-block: 0;
}
