/* Self-hosted Google Fonts, latin subsets only (covers English/Spanish).
   Google serves the variable files, so weights of a family/style share one
   woff2; each face gets its own block, as in Google's CSS. */
@font-face {
  font-family: "Lora";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/lora-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Lora";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/lora-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Lora";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/lora-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Lora";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/lora-italic-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Lora";
  font-style: italic;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/lora-italic-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/source-sans-3-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/source-sans-3-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Source Sans 3";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/source-sans-3-italic-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Tokens */
:root {
  --color-text: #e4e9ff;
  --color-background: #1e2fb8;
  --color-border: #4a58d4;
  --color-placeholder: #4150cc;
  --color-code-background: #2a3bc9;
  /* The one accent on the site. It stays put while the palette tokens above
     change with the theme, so a link reads the same everywhere. */
  --color-marker: #ffd23f;
  /* One drawn line, shared by the note leaders and the prose rule. A long tile
     with an uneven, shallow wobble: at a short tile and a single amplitude the
     repeat reads as a sine wave rather than as a line drawn by hand. */
  --rule-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='6' viewBox='0 0 240 6'%3E%3Cpath d='M0 3.1 C14 2.5 26 3.5 40 3.1 C54 2.7 64 2.4 78 2.9 C92 3.4 104 3.8 118 3.2 C130 2.7 140 2.5 154 3 C168 3.4 178 3.6 192 3.1 C206 2.6 216 2.5 228 2.95 C233 3.05 237 3.1 240 3.1' fill='none' stroke='%23000' stroke-width='1.05' stroke-linecap='round'/%3E%3C/svg%3E");
  --font-body: "Source Sans 3", sans-serif;
  --font-display: Lora, Georgia, serif;
  --font-mono: monospace;
  color: var(--color-text);
  background: var(--color-background);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
}

/* All three sections currently share the :root ultramarine; build.py still
   stamps a theme-* class on <html>, so a section can be given its own palette
   again by adding a `:root.theme-<name>` block here (see git history for the
   poppy/ochre versions). */

/* The section nav (Notes / Portfolio / CV), fixed top-right on every page. The
   current section is marked with the site's drawn marker underline. */
.site-nav {
  /* Absolute, not fixed: it rides at the top of the page and scrolls away with
     the content instead of sitting over it. (The portfolio plane doesn't
     scroll, so there it stays in view regardless.) */
  position: absolute;
  top: 22px;
  right: 26px;
  z-index: 20;
  display: flex;
  gap: 20px;
  font-family: var(--font-body);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .02em;
}
.site-nav a {
  position: relative;
  color: var(--color-text);
  text-decoration: none;
  opacity: .55;
  transition: opacity .2s;
}
.site-nav a:hover,
.site-nav a:focus-visible { opacity: 1; }
.site-nav a.is-active { opacity: 1; }
.site-nav a.is-active::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -5px;
  height: 6px;
  background-color: var(--color-marker);
  mask-image: var(--rule-mask);
  mask-repeat: repeat-x;
  mask-size: 240px 6px;
  mask-position: 0 50%;
}
/* The CV prints to PDF via print media; the nav is chrome, not content. */
@media print { .site-nav { display: none; } }

/* View transitions. The outgoing page fades out; the incoming one is left to
   the entry cascade below, so the two fades don't overlap and muddy each other.
   Browsers without support navigate normally. */
@view-transition { navigation: auto; }

::view-transition-group(*) {
  animation-duration: .28s;
  animation-timing-function: cubic-bezier(.2, 0, 0, 1);
}

::view-transition-new(root) { animation: none; }

/* Entry cascade. Elements arrive in reading order. `backwards` fill is
   required: without it an element sits visible through its delay, then snaps
   to transparent before fading in. */
@keyframes enter-stagger {
  from { opacity: 0; transform: translateY(6px); }
}

/* Images fade in place. Moving them reads as distracting against the quiet
   text cascade, and opacity alone lets the backdrop settle at its own value
   with no per-breakpoint rules. */
@keyframes enter-fade {
  from { opacity: 0; }
}

.intro h1,
.intro-copy p,
.post-list h2,
.post-row,
.back-link,
.post-heading,
.post-header time,
.prose {
  animation: enter-stagger .24s cubic-bezier(.2, 0, 0, 1) backwards;
}

.home-image,
.post-image {
  animation: enter-fade .2s cubic-bezier(.2, 0, 0, 1) backwards;
}

/* Homepage order: the image anchors the frame, text cascades into it. */
.home-image { animation-delay: 0ms; }
.intro h1 { animation-delay: 0ms; }
.intro-copy p:nth-child(1) { animation-delay: 20ms; }
.intro-copy p:nth-child(2) { animation-delay: 40ms; }
.intro-copy p:nth-child(3) { animation-delay: 60ms; }
.post-list h2 { animation-delay: 80ms; }

/* Post rows follow the heading. The catch-all lands first so posts beyond the
   ninth share the final delay instead of arriving before the ones above them. */
.post-row { animation-delay: 260ms; }
.post-list .post-row:nth-child(2) { animation-delay: 100ms; }
.post-list .post-row:nth-child(3) { animation-delay: 120ms; }
.post-list .post-row:nth-child(4) { animation-delay: 140ms; }
.post-list .post-row:nth-child(5) { animation-delay: 160ms; }
.post-list .post-row:nth-child(6) { animation-delay: 180ms; }
.post-list .post-row:nth-child(7) { animation-delay: 200ms; }
.post-list .post-row:nth-child(8) { animation-delay: 220ms; }
.post-list .post-row:nth-child(9) { animation-delay: 240ms; }

/* Post page order: a wider 60ms step than the homepage, since there are only
   a handful of elements and each one should read as its own beat.
   The backdrop image lands last here; on mobile it leads (see below). */
.back-link { animation-delay: 0ms; }
.post-heading { animation-delay: 60ms; }
.post-header time { animation-delay: 120ms; }
.prose { animation-delay: 180ms; }
.post-image { animation-delay: 240ms; }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }

  .intro h1,
  .intro-copy p,
  .post-list h2,
  .post-row,
  .back-link,
  .post-heading,
  .post-header time,
  .prose,
  .home-image,
  .post-image,
  .stitch-bg {
    animation: none;
  }

  /* Showing all three frames at once would overlap them, so hold the first. */
  .inline-icon g { animation: none; opacity: 0; }
  .inline-icon g:first-of-type { opacity: 1; }
}

/* Cross-stitch backdrop. The canvas draws the aida grid and the restitched
   cover (stitch-bg.js); it fades in on the image's beat. The original cover
   keeps its layout — the script measures its box — but stops painting once
   the stitched version has drawn, which is what `stitched` marks. */
.stitch-bg {
  position: fixed;
  inset: 0;
  /* A canvas is a replaced element: inset alone positions it but leaves it at
     its intrinsic bitmap size (viewport × dpr), which showed everything at 2×
     and pushed the stitched cover off the right edge. Size it explicitly. */
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
  animation: enter-fade .2s cubic-bezier(.2, 0, 0, 1) backwards;
}
.stitched .home-image img,
.stitched .post-image img { visibility: hidden; }

/* Base */
* { box-sizing: border-box; }
body { margin: 0; }
/* Links draw their own feedback (squiggles, spins); the platform's grey
   tap-flash rectangle on touch reads as a glitch, not feedback. */
a { color: inherit; -webkit-tap-highlight-color: transparent; }
h1, h2, .prose { font-family: var(--font-display); font-weight: 400; }
h1, h2, h3 { line-height: 1.25; text-wrap: balance; }
h1 { font-size: 1.55rem; margin: 0 0 24px; }
h2 { font-size: 1.1rem; font-weight: 400; margin: 84px 0 25px; }

/* Homepage */
.home-shell { min-height: 100vh; }

/* Anchored at 15vw with a 520px measure, the same as `.post-shell` and
   `.prose`, so the homepage and a note share one left edge. It used to be
   centred inside a grid column, which put it at an arbitrary offset and left a
   wide dead zone between the copy and the art. */
.home-content {
  width: min(100% - 56px, 520px);
  margin-left: 15vw;
  padding: clamp(72px, 12vh, 150px) 0 72px;
}

/* The homepage art gets the same ambient treatment as a note cover: fixed to
   the right edge, vertically centred, sitting behind the text at half strength.
   Kept at 70vh deliberately. Growing it to fill the space to its left was tried
   and abandoned: at that scale the tools crop through their own silhouettes and
   stop reading as objects. The empty middle is the better trade. */
.home-image {
  position: fixed;
  right: 9vw;
  top: 50%;
  transform: translateY(-50%);
  height: 70vh;
  width: auto;
  opacity: .5;
  pointer-events: none;
  z-index: -1;
  img { height: 70vh; width: auto; max-width: none; display: block; }
}

/* The waving arm: a cartoon arm parked top-left as a still-selected design
   tool object, selection chrome and all, waving at whoever reads the
   greeting. The chrome (outline, handles, size badge) holds still while the
   arm inside waves from the elbow, like artwork animating under a selection.
   The chrome blues are deliberately foreign to the palette: they belong to
   the imaginary editor, not to the site, so they must not follow the theme. */
.intro { position: relative; }
.hello-arm {
  /* Anchored to the intro so it rides beside the greeting at any viewport:
     past the title's text, bottom edge level with the title, reaching up
     into the page's top padding. Mirrored (on the img, so the chrome and
     badge stay un-mirrored) the hand waves up-right beside the name. */
  position: absolute;
  left: 235px;
  bottom: calc(100% - 52px);
  width: 115px;
  rotate: -16deg;
  outline: 1.5px solid #7db4ff;
  pointer-events: none;
  z-index: 1;
  transform-origin: 22% 72%;
  /* The whole selection waves as one, chrome and all, pivoting at the elbow.
     The wave animates `rotate`, so its keyframes carry the resting angle. */
  animation:
    enter-fade .2s cubic-bezier(.2, 0, 0, 1) 120ms backwards,
    arm-wave 1.5s cubic-bezier(.45, 0, .55, 1) infinite;
  img { display: block; width: 100%; transform: scaleX(-1); }
}
/* Sweep upward only: a wave rises from rest. Angles mirror the flip. */
@keyframes arm-wave {
  0%, 100% { rotate: -16deg; }
  50% { rotate: -7deg; }
}
.arm-handle {
  position: absolute;
  width: 10px;
  height: 10px;
  background: #fff;
  border: 1.5px solid #7db4ff;
}
.arm-handle-tl { top: -5px; left: -5px; }
.arm-handle-tr { top: -5px; right: -5px; }
.arm-handle-bl { bottom: -5px; left: -5px; }
.arm-handle-br { bottom: -5px; right: -5px; }
/* Inside the column it fits at any desktop width; mobile has no room. */
@media (max-width: 700px) { .hello-arm { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .hello-arm { animation: none; }
}

.intro-copy {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  line-height: 1.6;
  p { margin: 0 0 20px; text-wrap: pretty; }
}

/* Larger than a note title: this is the entry to the site, and at the shared
   1.55rem it read as just another line of text at the top of a column. */
.intro h1 { font-size: 2rem; margin-bottom: 28px; }

/* Set apart by the accent rather than by a different typeface. An uppercase
   letterspaced label was the first attempt and read as imported: nothing else
   on the site is uppercase, so it belonged to another design.
   The accent is shared with the section headings but deliberately not the same
   weight: thick and translucent marks structure, a thin solid rule marks
   something you can click. Giving both the marker made them compete. */
.contact {
  display: flex;
  gap: 22px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 34px;
}
/* A quiet lead-in, not a label. */
.contact > span { opacity: .55; }
.contact a {
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
}
/* The clickable underline, drawn instead of ruled: the site's wobble mask in
   marker, under the label only. On hover the tile steps sideways at the
   icons' flipbook cadence, so the line boils like a redrawn stroke. */
.contact-label { position: relative; }
.contact-label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -5px;
  height: 6px;
  background-color: var(--color-marker);
  mask-image: var(--rule-mask);
  mask-repeat: repeat-x;
  mask-size: 240px 6px;
  mask-position: 0 50%;
}
.contact a:hover .contact-label::after,
.contact a:focus-visible .contact-label::after {
  animation: contact-squiggle .375s steps(3) infinite;
}
@keyframes contact-squiggle {
  from { mask-position: 0 50%; }
  to { mask-position: -240px 50%; }
}
@media (prefers-reduced-motion: reduce) {
  .contact a:hover .contact-label::after,
  .contact a:focus-visible .contact-label::after,
  .post-row:hover .post-title::after,
  .post-row:focus-visible .post-title::after,
  .entry-icon.spinning,
  .entry-icon-placeholder.spinning { animation: none; }
}
/* Inlined like the note icons, so it takes the theme's text colour. `flex: none`
   stops it being squeezed when the row runs short. */
.contact-icon {
  width: 20px;
  height: 20px;
  flex: none;
  opacity: .7;
  overflow: visible;
}

/* Section headings carry the marker as a short drawn stroke beneath the words.
   The earlier full-width highlighter band ran through the glyphs, which worked
   on paper but turned into a slab under light-on-dark themes. A short thick
   dash keeps the structure/click distinction (thick marks structure, a thin
   full-length rule marks a link) without touching the letters. It borrows the
   leaders' wobble mask, scaled 4x tall so the line comes out marker-thick.
   Shared by the note sections and the homepage list, so both pages carry the
   one accent on the site. */
.prose h2,
.post-list h2 {
  &::after {
    content: "";
    display: block;
    width: 48px;
    height: 6px;
    margin-top: 6px;
    background-color: color-mix(in srgb, var(--color-marker) 88%, transparent);
    mask-image: var(--rule-mask);
    mask-repeat: no-repeat;
    /* 240x6 art scaled to 240x24: the line thickens to ~4px, and pulling the
       tile up 9px centres that line in this 6px-tall box. */
    mask-size: 240px 24px;
    mask-position: 0 -9px;
  }
}

/* Post list */
.post-row {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 42px;
  text-decoration: none;
  font-size: .95rem;
  position: relative;
  .post-title { white-space: nowrap; position: relative; }
  /* The same drawn squiggle the contact links use, in ink rather than
     marker: a note is a destination, not the site's one accent. */
  .post-title::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -3px;
    height: 6px;
    background-color: var(--color-text);
    mask-image: var(--rule-mask);
    mask-repeat: repeat-x;
    mask-size: 240px 6px;
    mask-position: 0 50%;
    opacity: 0;
  }
  &:hover .post-title::after,
  &:focus-visible .post-title::after {
    opacity: 1;
    animation: contact-squiggle .375s steps(3) infinite;
  }
  /* A drawn line rather than a dotted border, to sit with the hand-drawn
     icons. The wobble is an svg used as a mask over the border colour: a
     data-uri background cannot resolve `currentColor`, so masking is what
     keeps the leader following the theme. */
  .post-rule {
    flex: 1;
    height: 6px;
    transform: translateY(2px);
    background-color: var(--color-border);
    mask-image: var(--rule-mask);
    mask-repeat: repeat-x;
    mask-size: 240px 6px;
    mask-position: center;
  }
  time { white-space: nowrap; }
}

/* Size and colour only. Each context places it, because the listing sets the
   icon in the flow while a note title hangs it in the margin.
   The icon is inlined svg, so it inherits `color` and tracks the theme, and
   `overflow: visible` keeps strokes on the viewBox edge from being clipped. */
.entry-icon {
  width: 30px;
  height: 30px;
  color: var(--color-text);
  opacity: .8;
  overflow: visible;
}
.entry-icon-placeholder {
  width: 30px;
  height: 30px;
  background: radial-gradient(circle, var(--color-placeholder) 1px, transparent 1.5px);
  background-size: 5px 5px;
}

/* In the listing the icon leads the row rather than hanging beside it, so the
   column has one left edge: the greeting, the copy, the section heading and
   every icon all start on it. The titles indent behind the icon as a result. */
.post-row .entry-icon,
.post-row .entry-icon-placeholder { flex: none; margin-right: 4px; }

/* Entering a note row spins its icon once about the vertical axis. The
   perspective inside the transform is what sells the flat drawing as a coin:
   the near half swells and the far half shrinks as it turns. The class is
   pinned by spin-icons.js from row entry until the turn completes, rather
   than riding :hover — a hover animation dies with the hover, so a quick
   sweep down the list spun nothing. Ending at 360deg lands exactly on the
   resting pose, so nothing ever plays backwards. */
.entry-icon.spinning,
.entry-icon-placeholder.spinning {
  animation: icon-spin .55s cubic-bezier(.35, 0, .25, 1);
}
@keyframes icon-spin {
  from { transform: perspective(200px) rotateY(0deg); }
  to { transform: perspective(200px) rotateY(360deg); }
}

/* Worked with: every logo rides on the blue as a plain white mark
   (brightness(0) flattens any color to black, invert flips it white).
   Original colors were tried on hover and abandoned: most of these marks
   are black or brand-colored and unreadable straight on the blue, and a
   backing chip read as a foreign block. Hover just resolves the fade,
   the same move as the back arrow. */
.worked-at .logo-row {
  max-width: 520px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 28px 36px;
}
.worked-logo {
  display: inline-flex;
  opacity: .7;
  transition: opacity .16s cubic-bezier(.2, 0, 0, 1);
  img { width: auto; filter: brightness(0) invert(1); }
  &:hover, &:focus-visible { opacity: 1; }
}

/* Post page */
.post-shell { max-width: 1040px; margin: 0; padding: 48px 44px 100px 15vw; }
.back-link {
  position: fixed;
  left: 3.5vw;
  top: 46px;
  display: inline-flex;
  text-decoration: none;
  opacity: .6;
  transition: opacity .16s cubic-bezier(.2, 0, 0, 1);
  /* An underline has nothing to sit under now that the arrow is a glyph-less
     icon, so hover resolves the fade instead. */
  &:hover, &:focus-visible { opacity: 1; }
}
.back-icon { width: 36px; height: 36px; overflow: visible; }
.post { max-width: 990px; margin: 0; }
.post-header { max-width: 520px; margin: 0 0 38px; }
/* `position: relative` is what makes the icon work: it's absolutely positioned
   at left: -22px so it hangs in the left margin, and without a positioned
   ancestor here it resolved against the viewport instead. Keeping it out of
   the flow leaves the title flush left with the date and body. */
.post-heading {
  position: relative;
  display: flex;
  align-items: center;
  h1 { margin: 0; font-size: 1.55rem; }
  /* Still hung in the margin here: a note has one title, and letting it sit
     flush keeps the h1 aligned with the date and body text beneath it. */
  .entry-icon, .entry-icon-placeholder {
    position: absolute;
    left: -35px;
    top: 50%;
    transform: translateY(-50%);
    width: 28px;
    height: 28px;
  }
}
.post-header time { display: block; margin-top: 12px; font-size: .9rem; }
/* The colophon above every note: how the words got here. Quiet enough to be
   skippable, closed off from the body by the same drawn rule the prose uses. */
.post-note {
  max-width: 520px;
  margin: -14px 0 40px;
  font-size: .9rem;
  opacity: .6;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
.post-note p { flex: 1; min-width: 0; margin: 0; }
.post-note .note-icon {
  width: 26px;
  height: 26px;
  flex: none;
  margin-top: 1px;
  overflow: visible;
}
.post-note::before,
.post-note::after {
  content: "";
  display: block;
  width: 100%;
  height: 6px;
  background-color: var(--color-border);
  mask-image: var(--rule-mask);
  mask-repeat: repeat-x;
  mask-size: 240px 6px;
}
.post-note::before { margin-bottom: 6px; }
.post-note::after { margin-top: 6px; }

/* The short version of a note, from its `tldr:` front matter: a light card
   wider than the text column, with a marker-yellow tab hanging off its top
   edge. The card is the one light surface on the page, so its colors are its
   own rather than tokens. */
.post-tldr {
  position: relative;
  max-width: 572px;
  margin: 6px 0 40px -26px;
  background: #f3f2f7;
  color: #1a1a1a;
  border-radius: 8px;
  padding: 28px 26px 22px;
  p {
    margin: 0;
    font-family: var(--font-display);
    font-size: .9rem;
    line-height: 1.5;
    text-wrap: pretty;
  }
}
.post-tldr-tab {
  position: absolute;
  top: -15px;
  left: 34px;
  background: var(--color-marker);
  color: #1a1a1a;
  font: 600 .75rem var(--font-body);
  letter-spacing: .02em;
  padding: 6px 14px;
  border-radius: 4px;
}

/* Aligned by their right edge with a margin, never cropped. Covers vary in
   aspect ratio, so anything width-relative here (a percentage translate, an
   overhang) placed each one differently; a fixed right offset is the one rule
   that holds whatever shape the next cover turns out to be. Tune `right`. */
.post-image {
  position: fixed;
  right: 9vw;
  top: 50%;
  transform: translateY(-50%);
  height: 70vh;
  width: auto;
  max-width: none;
  opacity: .5;
  margin: 0;
  float: none;
  pointer-events: none;
  z-index: -1;
  img { height: 70vh; width: auto; max-width: none; display: block; }
}

/* Opted into per note with `cover: bleed`. Anchored from the left and allowed
   to run off the right edge, which suits a screenshot: it reads as a window
   carrying on past the frame. A cut-out object cropped the same way just looks
   sliced, which is why this is declared rather than applied to everything.
   Anchoring by `left` also keeps the visible portion the same whatever the
   image's proportions, where a negative `right` would crop each one differently. */
/* `cover: small` for a cover that carries more ink than the rest and so reads
   heavier at the shared size. Height is the only lever, since width follows. */
.post-image-small {
  height: 55vh;
  img { height: 55vh; }
}

.post-image-bleed {
  /* Same 70vh as every other cover, so bleeding is purely a matter of where it
     sits. The anchor is further right than it would be for a contained cover,
     because at this size a 60% anchor only overhangs by a couple of percent and
     reads as bad alignment rather than as a deliberate crop. */
  left: 65%;
  right: auto;
}
/* Prose type. The face is Lora, whose webfont carries 400/500/600 only, so
   `strong` is pinned to 600: at the browser default of 700 there is no real
   face to reach for and it synthesises a smeared fake bold instead. */
.prose {
  max-width: 520px;
  font-size: 1.0625rem;
  line-height: 1.68;

  p { margin: 0 0 1.15em; text-wrap: pretty; }

  strong { font-weight: 600; }
  em { font-style: italic; }
  /* Nested emphasis needs the italic 600 face, which the import now loads. */
  strong em, em strong { font-weight: 600; font-style: italic; }

  h2, h3 {
    font-weight: 500;
    line-height: 1.3;
    margin-top: 2.2em;
    margin-bottom: .5em;
  }
  h2 { font-size: 1.2rem; }
  h3 { font-size: 1.05rem; }
  /* A subheading directly under its heading is one unit, not two sections. */
  h2 + h3 { margin-top: 1.1em; }
  /* A note opening on a heading should not be pushed off its own top margin. */
  :is(h2, h3):first-child { margin-top: 0; }

  /* The underline sits back at border weight so a paragraph with several links
     does not read as striped, and resolves to full strength on hover. */
  a {
    text-decoration-thickness: 1px;
    text-underline-offset: .18em;
    text-decoration-color: var(--color-border);
    transition: text-decoration-color .15s cubic-bezier(.2, 0, 0, 1);
    &:hover, &:focus-visible { text-decoration-color: currentColor; }
  }

  ul, ol { margin: 0 0 1.3em; padding-left: 1.3em; }
  /* Items often run to several lines behind a bold lead-in now, so they want
     something closer to paragraph spacing than to list spacing. */
  li { margin-bottom: .6em; padding-left: .25em; text-wrap: pretty; }
  li:last-child { margin-bottom: 0; }
  /* Muted, so the markers organise the list without competing with the text. */
  li::marker { color: var(--color-border); }

  blockquote {
    margin: 1.6em 0;
    padding-left: 1.1em;
    border-left: 2px solid var(--color-border);
    font-style: italic;
  }

  /* The same drawn line the note listing uses for its leaders, so the two
     kinds of rule on the site are one mark. */
  hr {
    margin: 2.4em 0;
    border: 0;
    height: 6px;
    background-color: var(--color-border);
    mask-image: var(--rule-mask);
    mask-repeat: repeat-x;
    mask-size: 240px 6px;
    mask-position: center;
  }

  img, video {
    max-width: 100%;
    width: 100%;
    height: auto;
    display: block;
    border-radius: 4px;
  }
  /* Two hairlines with a gap between them. The outer ring is an outline rather
     than a second element or a spread shadow, so the gap stays genuinely
     transparent and the backdrop cover shows through it, instead of a painted
     band that would have to track the theme background. */
  img {
    border: 1px solid var(--color-border);
    outline: 1px solid var(--color-border);
    outline-offset: 3px;
  }
  /* Video takes a single dotted hairline instead, so the two kinds of media
     are told apart by their frame. */
  video { border: 1px dotted var(--color-border); }

  /* Opted into per embed with `.bare`. Media that is already transparent, or
     that carries the page's own background, is boxing nothing: the frame ends
     up drawing a rectangle around the page showing through. */
  .bare {
    border: 0;
    outline: 0;
    border-radius: 0;
  }

  /* Both gaps carry an extra 4px: the outline ring sits outside the image's
     box without taking up space, so the optical gap would otherwise close by
     exactly its offset plus its width. A bare embed has no ring to allow for. */
  /* The optical gap below an image matches the text rhythm (p's 1.15em is
     about 20px), with the framed variant carrying the ring's extra 4px.
     Two shapes to cover: a markdown embed is wrapped in a paragraph, while a
     classed embed compiles to a raw tag that markdown treats as an html
     block, landing as a direct child of .prose with no wrapper. */
  p:has(> img:only-child) { margin: 0 0 24px; line-height: 0; }
  p:has(+ p:has(> img:only-child)) { margin-bottom: 12px; }
  p:has(> .bare:only-child) { margin-bottom: 20px; }
  > img, > video { margin: 0 0 24px; }
  > img.bare, > video.bare { margin-bottom: 20px; }

  iframe { width: 100%; min-height: 300px; border: 0; }

  pre {
    overflow-x: auto;
    margin: 1.6em 0;
    padding: 16px;
    font-family: var(--font-mono);
    font-size: .85rem;
    line-height: 1.5;
    background: var(--color-code-background);
  }
  code {
    font-family: var(--font-mono);
    font-size: .85em;
    padding: .1em .35em;
    border-radius: 3px;
    background: var(--color-code-background);
  }
  /* The block already carries that background; the inline treatment must not
     stack a second one inside it. */
  pre code { padding: 0; background: none; font-size: 1em; }
}

/* Mobile */
@media (max-width: 700px) {
  .home-shell { display: flex; flex-direction: column; }
  /* Same ambient backdrop the note covers use at this width: cropped off the
     right edge, behind the text rather than a block above it. */
  .home-image {
    position: fixed;
    top: 10%;
    left: 30%;
    right: auto;
    height: 50%;
    width: auto;
    transform: none;
    opacity: .5;
    pointer-events: none;
    z-index: -1;
    img { height: 100%; width: auto; max-width: none; }
  }
  /* Extra top room so the greeting clears the fixed section nav. */
  .home-content { order: 1; width: auto; margin: 0; padding: 66px 28px 52px; }
  h1 { margin-bottom: 25px; }
  h2 { margin-top: 80px; }
  .intro-copy { font-size: 1.0625rem; }
  .post-row {
    min-height: 42px;
    gap: 5px;
    /* The title is nowrap, and a nowrap flex child refuses to shrink below
       its full text, so a long title pushes the date off the edge here.
       min-width lets it shrink, the ellipsis marks the cut. The overflow
       clip also catches the squiggle, which hangs 3px below the box — and a
       tap's sticky hover DOES show it on touch — so the box grows 3px of
       padding for it to live in (margin compensating the row's centring)
       and the squiggle anchors inside: same visual spot, no longer cut. */
    .post-title {
      font-size: .95rem;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      padding-bottom: 3px;
      margin-bottom: -3px;
    }
    .post-title::after { bottom: 0; }
    time { font-size: .9rem; }
  }
  .post-shell { padding: 35px 26px 60px; }
  /* The arrow is drawn ~57/512 into its viewBox, so at 36px the glyph starts
     ~4px in. Pulled back so its visible edge sits on the text edge below. */
  .back-link { position: static; margin: 0 0 38px -4px; }
  /* No margin to hang in at this width, so the icon rejoins the flow above
     the title. Beside it, a two-line title indents behind the icon and the
     header loses the one left edge everything else sits on; stacked, icon,
     title, date and body all share it, whatever the title's length. */
  .post-heading { flex-direction: column; align-items: flex-start; gap: 16px; }
  .post-heading .entry-icon,
  .post-heading .entry-icon-placeholder {
    position: static;
    transform: none;
  }
  .post { display: flex; flex-direction: column; }
  /* An ambient backdrop here too, not a block above the title: it sits behind
     the text, cropped off the right edge. `right: auto` is required, since the base
     rule pins it to `right: 0`, and left+right together would stretch it. */
  .post-image {
    position: fixed;
    top: 10%;
    left: 30%;
    right: auto;
    height: 50%;
    width: auto;
    max-width: none;
    margin: 0;
    float: none;
    transform: none;
    opacity: .5;
    pointer-events: none;
    z-index: -1;
  }
  .post-image img { height: 100%; width: auto; max-width: none; }
  .post-header { order: 1; margin-bottom: 24px; }
  /* Between header and prose here too: without an order it would land first. */
  .post-note { order: 2; }
  /* Shares the colophon's order and follows it in the source, so it keeps its
     slot. No margin to overhang into at this width, so the card sits flush. */
  .post-tldr { order: 2; margin-left: 0; }
  .prose { order: 3; font-size: 1.0625rem; line-height: 1.65; }

}
