@import url("https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400;1,600&family=Source+Sans+3:ital,wght@0,400;0,600;1,400&display=swap");

/* 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;
}

/* 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 {
    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; }
}

/* Base */
* { box-sizing: border-box; }
body { margin: 0; }
a { color: inherit; }
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; }
}

.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-line: underline;
  text-decoration-color: var(--color-marker);
  text-decoration-thickness: 2px;
  text-underline-offset: .2em;
  transition: text-underline-offset .15s cubic-bezier(.2, 0, 0, 1);
  &:hover, &:focus-visible { text-underline-offset: .14em; }
}
/* 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: a thick translucent stroke pulled up by a
   negative offset so it rides over the foot of the words. Underlines paint
   behind the glyphs, which is what keeps the heading readable through it, and
   `skip-ink: none` runs it unbroken through descenders like a real highlighter.
   Shared by the note sections and the homepage list, so both pages carry the
   one accent on the site. */
.prose h2,
.post-list h2 {
  text-decoration-line: underline;
  /* 75% rather than something more translucent: over a dark ground a weaker
     mix composites toward grey-brown and stops reading as marigold at all. */
  text-decoration-color: color-mix(in srgb, var(--color-marker) 75%, transparent);
  text-decoration-thickness: .42em;
  text-underline-offset: -.14em;
  text-decoration-skip-ink: none;
}

/* Post list */
.post-row {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 42px;
  text-decoration: none;
  font-size: .95rem;
  position: relative;
  &:hover .post-title { text-decoration: underline; }
  .post-title { white-space: nowrap; }
  /* 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; }

/* 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; }
/* 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. */
  p:has(> img:only-child) { margin: 0 0 14px; line-height: 0; }
  p:has(+ p:has(> img:only-child)) { margin-bottom: 12px; }
  p:has(> .bare:only-child) { margin-bottom: 10px; }

  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; }
  }
  .home-content { order: 1; width: auto; margin: 0; padding: 36px 28px 52px; }
  h1 { margin-bottom: 25px; }
  h2 { margin-top: 80px; }
  .intro-copy { font-size: 1.0625rem; }
  .post-row { min-height: 42px; gap: 5px; .post-title { font-size: .95rem; } time { font-size: .9rem; } }
  .post-shell { padding: 35px 26px 60px; }
  .back-link { position: static; margin-bottom: 38px; }
  .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; }
  .prose { order: 2; font-size: 1.0625rem; line-height: 1.65; }

}
