/* ==========================================================================
   jessemaddox.com, "paper" prototype.

   One typeface (Literata, variable, self-hosted, roman + italic), one warm
   ground, one non-blue accent. Hierarchy comes from size, weight, case and
   colour only. No sans anywhere on the page, deliberately.

   Every colour is derived from a single seed with OKLCH relative colour
   functions. Nothing here was picked off a swatch panel.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. The typeface
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Literata";
  src: url("/assets/ds/fonts/literata-var.woff2") format("woff2");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Literata";
  src: url("/assets/ds/fonts/literata-italic-var.woff2") format("woff2");
  font-weight: 200 900;
  font-style: italic;
  font-display: swap;
}


/* --------------------------------------------------------------------------
   2. The palette, derived

   --seed is a terracotta. Everything else is that seed moved in OKLCH:
     paper       lighter, almost decoloured, hue rotated 42deg toward cream
     ink         much darker, decoloured, hue rotated 101deg into green-grey,
                 so the near-black has a hue in it and is never #000
     accent      the seed, a shade deeper, used on link underlines and the
                 footnote star and nowhere else
   -------------------------------------------------------------------------- */

:root {
  --seed: oklch(0.58 0.152 41);

  --paper:       oklch(from var(--seed) calc(l + .375) calc(c - .140) calc(h + 42));
  --paper-2:     oklch(from var(--paper) calc(l - .030) calc(c + .004) h);
  --ink:         oklch(from var(--seed) calc(l - .320) calc(c - .135) calc(h + 101));
  --ink-soft:    oklch(from var(--ink) calc(l + .170) c h);
  --ink-faint:   oklch(from var(--ink) calc(l + .320) c h);
  --rule:        oklch(from var(--ink) calc(l + .535) c h);
  --accent:      oklch(from var(--seed) calc(l - .060) calc(c + .010) h);

  /* Motion. No keyword easings, no `transition: all`, anywhere. */
  --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);

  /* The row marks. --glyph is the box each illustration is drawn into, and
     --sw is the resting engraving's stroke width in hundredths of that box,
     so it scales with the mark. 4 units at a 36px glyph is 1.44 device pixels,
     which is a Literata stem at this body size. The comparison sheet drew
     these at 3.2, which is 1.15px: correct as a test, a shade light in the
     margin of the type it is standing next to. */
  --glyph: 36px;
  --sw: 4;
  /* One of the nine arrives in its own 39.6 unit coordinate space and is
     placed with a scale(2.41), which would multiply its stroke by the same.
     This is that stroke pre-divided, so it comes out at the weight of the
     other eight. 4 / 2.41. */
  --sw-scaled: 1.66;

  /* 36rem outer, which after the gutters leaves a 32rem text column. */
  --measure: 36rem;

  /* Vertical rhythm. These are the compact set, tuned so the whole page,
     footnote included, clears a 700px viewport with nothing below the fold.
     A taller viewport gets the roomier set further down.

     --row-pad was .22rem before the marks, then .16rem to pay for the line the
     marks cost: they take about 21px out of the index, enough to break the
     longest pulled quotation onto a second line. Retiring the crossword cell
     rule bought that height back, so this sits at .20rem, one notch off the
     original rhythm rather than three. Slack at 1100x700 is +15px. */
  --fs-body: clamp(1rem, .95rem + .25vw, 1.0625rem);
  --fs-title: 1.05em;
  --fs-meta: .78em;
  --lh-title: 1.22;
  --lh-meta: 1.36;
  --row-pad: .20rem;
  --step: 1rem;
}

/* Only spend the extra air when there is enough height that the whole page
   still lands above the fold with it. 58rem is 928px: a 1440x900 window stays
   on the compact set, a taller one relaxes. */
@media (min-height: 58rem) {
  :root {
    --fs-body: clamp(1.0625rem, 1rem + .28vw, 1.1875rem);
    --fs-title: 1.0625em;
    --fs-meta: .82em;
    --lh-title: 1.28;
    --lh-meta: 1.42;
    --row-pad: .42rem;
    --step: 1.6rem;
  }
}

/* Fallbacks for engines without relative colour (pre Chrome 119 / Safari 16.4).
   These hex values are not choices: they are the computed output of the
   derivations above, so the two paths agree. */
@supports not (color: oklch(from white l c h)) {
  :root {
    --paper: #f4f0e7;
    --paper-2: #ebe5db;
    --ink: #1f261f;
    --ink-soft: #4a5349;
    --ink-faint: #747d73;
    --rule: #b7bfb6;
    --accent: #b13d00;
  }
}


/* --------------------------------------------------------------------------
   3. Reset and global type, after TODS
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: none;
  text-size-adjust: none;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);

  /* Sit the block in the middle of the window when it fits. `safe` is the
     whole point: the moment the page is taller than the viewport, alignment
     falls back to start and nothing is clipped off the top. Engines without
     `safe` drop the declaration and get start alignment, which is the same
     answer. */
  min-height: 100svh;
  display: grid;
  align-content: safe center;

  font-family: "Literata", Georgia, "Times New Roman", serif;
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.55;

  font-optical-sizing: auto;
  font-kerning: normal;
  font-variant-ligatures: common-ligatures contextual;
  font-variant-numeric: oldstyle-nums proportional-nums;
  font-variant-emoji: text;
  -webkit-font-smoothing: antialiased;

  /* Safari only. Everywhere else it is simply absent, which is fine. */
  hanging-punctuation: first last;

  /* Real curly quotes come from the `q` element, not from the markup. */
  quotes: "\201C" "\201D" "\2018" "\2019";
}

h1, p, ol, li, figure { margin: 0; }
/* role="list" is on the element too: Safari drops list semantics when the
   marker is removed, and this list is the whole page. */
ol { padding: 0; list-style: none; }
/* The skip link's target. It takes focus programmatically, so it must not
   also draw a ring. The first row does that a Tab later. */
.index:focus { outline: none; }

::selection {
  background: color-mix(in srgb, var(--accent), transparent 78%);
  color: var(--ink);
}


/* --------------------------------------------------------------------------
   4. Film grain

   feTurbulence, three octaves, rasterised once into a data URI. Fixed,
   pointer-events none, never animated. It is doing about four percent of
   the work and should never be noticed on purpose.

   The feColorMatrix is load-bearing. Raw feTurbulence is coloured speckle,
   which at any visible strength looks like a broken JPEG. The matrix throws
   away the colour and rebuilds the noise as pure black at a varying alpha
   (a = r + g + b - 1), so what lands on the paper is neutral tooth rather
   than confetti, and four percent of it is actually perceptible.
   Measured: it moves the ground about two percent darker and introduces a
   luminance standard deviation of ~1.9 of 255 across a flat field.
   -------------------------------------------------------------------------- */

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9;
  pointer-events: none;
  opacity: .04;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 -1'/%3E%3C/filter%3E%3Crect width='260' height='260' filter='url(%23g)'/%3E%3C/svg%3E");
}


/* --------------------------------------------------------------------------
   5. Links

   Chris Coyier's structure: the resting state is the exception, so hover and
   focus need no rule of their own to restore full-strength colour.
   -------------------------------------------------------------------------- */

a {
  color: inherit;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
  text-decoration-skip-ink: auto;
  transition: text-decoration-color 200ms var(--ease-out-quint);
}

a:not(:is(:hover, :focus)) {
  text-decoration-color: color-mix(in srgb, currentColor, transparent 75%);
}

a:is(:hover, :focus) { text-decoration-color: var(--accent); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 1px;
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent), transparent 84%);
}
:focus:not(:focus-visible) { outline: none; }

.skip {
  position: absolute;
  left: 1rem;
  top: -4rem;
  z-index: 20;
  padding: .6rem 1rem;
  background: var(--paper-2);
  border: 1px solid var(--ink);
  transition: top 220ms var(--ease-out-quint);
}
.skip:focus { top: 1rem; }


/* --------------------------------------------------------------------------
   6. Page frame
   -------------------------------------------------------------------------- */

.sheet {
  max-width: var(--measure);
  margin-inline: auto;
  padding: calc(var(--step) * 1.75) clamp(1.25rem, .5rem + 3vw, 2rem)
           calc(var(--step) * 1.25);
}


/* --------------------------------------------------------------------------
   7. The name

   The crossword grid used to sit here as a page-level motif and also as the
   section rule below. It now appears exactly once, as the row mark for the
   crossword itself, so the wordmark stands on its own.
   -------------------------------------------------------------------------- */

.namebar {
  display: flex;
  align-items: flex-end;
  gap: clamp(.7rem, .4rem + 1vw, 1.05rem);
}

.name {
  font-size: clamp(1.95rem, 1.55rem + 1.75vw, 2.9rem);
  font-weight: 460;
  line-height: 1.08;
  letter-spacing: -0.012em;
  text-wrap: balance;
  font-variant-ligatures: discretionary-ligatures common-ligatures contextual;
  font-variant-numeric: lining-nums;
}

.star {
  color: var(--accent);
  font-size: .42em;
  font-weight: 500;
  vertical-align: super;
  line-height: 0;
  margin-inline-start: .06em;
}

.intro {
  margin-block-start: calc(var(--step) * .65);
  max-width: 31rem;
  color: var(--ink-soft);
  line-height: 1.58;
  text-wrap: pretty;
}


/* --------------------------------------------------------------------------
   8. The section rule

   A plain hairline, the same one the footer uses. One rule system on the page,
   not two.
   -------------------------------------------------------------------------- */

.rule {
  width: min(11.875rem, 100%);
  height: 0;
  margin-block: var(--step);
  margin-inline: 0;
  border: 0;
  border-block-start: 1px solid var(--rule);
}


/* --------------------------------------------------------------------------
   9. The index

   Each row is one link, so the whole two-line block is the target. The
   underline lives on the title alone; the resting-state exception is written
   against the row so hover and focus still need no extra rule.
   -------------------------------------------------------------------------- */

.row {
  display: block;
  position: relative;
  padding: var(--row-pad) 0 var(--row-pad) calc(var(--glyph) + .62rem);
  text-decoration: none;
  transition: transform 260ms var(--ease-out-quint);
}

.stack { display: block; }

.title {
  display: block;
  font-size: var(--fs-title);
  font-weight: 500;
  line-height: var(--lh-title);
  letter-spacing: -0.004em;
  text-wrap: pretty;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
  text-decoration-skip-ink: auto;
  transition: text-decoration-color 200ms var(--ease-out-quint);
}
.row:not(:is(:hover, :focus)) .title {
  text-decoration-color: color-mix(in srgb, currentColor, transparent 75%);
}
.row:is(:hover, :focus) .title { text-decoration-color: var(--accent); }

/* The typed metadata line. Real small caps from Literata's smcp/c2sc, never
   text-transform. Tabular lining figures so the years hold a column. */
.meta {
  display: block;
  margin-block-start: .1em;
  color: var(--ink-soft);
  font-size: var(--fs-meta);
  line-height: var(--lh-meta);
  font-variant-caps: all-small-caps;
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: .045em;
  text-wrap: pretty;
}

.sep {
  color: var(--ink-faint);
  padding-inline: .5em;
}

/* The pulled quotation drops out of small caps and back into ordinary
   sentence case, in italic. Quote marks come from the `quotes` property. */
.pull {
  font-style: italic;
  font-variant-caps: normal;
  font-variant-numeric: oldstyle-nums proportional-nums;
  letter-spacing: 0;
  color: var(--ink-soft);
}


/* --------------------------------------------------------------------------
   10. The row marks

   One small illustration per row, in the slot on the left. Each is drawn
   twice into the same box: a fine line engraving, which is what you see, and
   a solid silhouette of the same subject, which is at zero opacity until the
   row is hovered or focused. Then they cross-fade and the mark inks in.

   Two rules the drawings obey. Every one is a 0 0 100 100 viewBox, so a
   stroke width is a fraction of the rendered size and the whole set can be
   retuned from --sw in one place. And no colour is written into the markup:
   the ink comes from currentColor, and the two places a drawing needs the
   ground colour to cut a hole in itself, it asks for --paper.
   -------------------------------------------------------------------------- */

.glyph {
  position: absolute;
  left: 0;
  top: 50%;
  /* The `translate` property rather than a transform, so this cannot collide
     with the transform the hover puts on the row. Centred on the whole two
     line row, not hung off the first line. */
  translate: 0 -50%;
  width: var(--glyph);
  height: var(--glyph);
  color: var(--ink);
}

/* Both drawings occupy the same box. Stacked, not laid out. */
.ico {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* The engraving is the resting state, the silhouette the inked one. Only the
   opacities are stated here; the transition between them is opt-in, further
   down, so reduced motion and a failed stylesheet both still get the swap. */
.ico-b { opacity: 1; }
.ico-a { opacity: 0; }

/* Keyboard focus inks the mark on every device, pointer or not. */
.row:focus-visible .ico-b { opacity: 0; }
.row:focus-visible .ico-a { opacity: 1; }

/* A pointer hover does the same. Gated, so a tap on a phone cannot leave a
   row stuck inked. The pair is written out in both places rather than shared,
   because the two paths must not be able to drift apart. */
@media (hover: hover) and (pointer: fine) {
  .row:is(:hover, :focus-visible) .ico-b { opacity: 0; }
  .row:is(:hover, :focus-visible) .ico-a { opacity: 1; }
}

@media (prefers-reduced-motion: no-preference) {
  .ico { transition: opacity 165ms var(--ease-out-quint); }
}

/* The drawing vocabulary.

   B, the engraving. One weight, no fill. The two exceptions are honest ones:
   a die's pip and a vice's screw cannot be drawn as outlines at this size, so
   they are cut as solid dots, the way an engraver would. */
.b-line {
  fill: none;
  stroke: currentColor;
  stroke-width: var(--sw, 4);
  stroke-linejoin: round;
  stroke-linecap: round;
}
.b-dot { fill: currentColor; stroke: none; }
/* The one drawing that is placed with a transform. See --sw-scaled. */
.scaled { --sw: var(--sw-scaled); }
/* Negative space carried under an outline, so the shape on top reads as
   sitting in front of the one behind rather than tangling with it. Twice the
   line width leaves a clear gap of paper outside the line it is clearing. */
.b-moat {
  fill: var(--paper);
  stroke: var(--paper);
  stroke-width: calc(var(--sw, 4) * 2);
  stroke-linejoin: round;
}

/* A, the silhouette. Filled shapes and knockouts. `.s-line` is a stroke used
   as a solid rod rather than as an outline, which is why it carries no width
   of its own and states one where it is used. */
.s-fill  { fill: currentColor; stroke: none; }
.s-line  { fill: none; stroke: currentColor; stroke-linejoin: round; }
.s-knock { fill: var(--paper); stroke: none; }
/* A same-colour round-join stroke on a filled shape, softening a corner the
   rest of the set would not have cut that sharply. */
.s-soft  { stroke: currentColor; stroke-linejoin: round; stroke-linecap: round; }
.s-moat  { fill: var(--paper); stroke: var(--paper); stroke-linejoin: round; }


/* --------------------------------------------------------------------------
   11. Footer
   -------------------------------------------------------------------------- */

/* One line where there is room, two where there is not. */
.foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: .35em 1.75em;
  margin-block-start: calc(var(--step) * 1.1);
  padding-block-start: calc(var(--step) * .7);
  border-block-start: 1px solid var(--rule);
  font-size: .84em;
  color: var(--ink-soft);
}

.label {
  font-variant-caps: all-small-caps;
  letter-spacing: .06em;
  color: var(--ink-faint);
  margin-inline-end: .55em;
}

.note {
  color: var(--ink-faint);
  text-wrap: pretty;
}
.note .star {
  vertical-align: baseline;
  font-size: 1em;
  line-height: inherit;
  margin-inline-end: .3em;
}


/* --------------------------------------------------------------------------
   12. Sparkle

   The staggered reveal is opt-in by construction. The base rule sets
   `animation: none`, and the keyframed version exists only inside the
   no-preference query, so reduced motion and a failed stylesheet both land on
   a fully visible, static page. Nothing is hidden by default.
   -------------------------------------------------------------------------- */

.reveal { animation: none; }

@media (prefers-reduced-motion: no-preference) {
  .reveal {
    animation: rise 600ms var(--ease-out-quint) both;
    animation-delay: calc(var(--stagger, 0) * 120ms);
  }
  @keyframes rise {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
  }
}

/* Hover is geometry, not colour. Pointer devices only, so a touch tap never
   sticks in a hover state. */
@media (hover: hover) and (pointer: fine) {
  .row:hover { transform: translateX(2px); }
  .row:active {
    transform: scale(0.98);
    transform-origin: left center;
    transition-duration: 90ms;
  }
}


/* --------------------------------------------------------------------------
   13. Small screens: 44px targets

   The mark comes down from 36 to 30. Not because 30 is a nicer number: a
   phone row is 57 to 74 pixels tall, so 36 still fits vertically, but the
   text column at 390 is 350 pixels wide and a 36 pixel mark with its gutter
   takes 46 of them. At 30 the marks still read, and the four titles that were
   one word from wrapping stop wrapping. The engraving is retuned to match, at
   4.8 units, so the stroke lands at the same 1.44 device pixels it does on a
   desktop and the mark does not thin out as the box shrinks.
   -------------------------------------------------------------------------- */

@media (max-width: 34rem) {
  :root {
    --glyph: 30px;
    --sw: 4.8;
    --sw-scaled: 1.99;
  }
  .row {
    padding-block: .55rem;
    min-height: 44px;
  }
  /* The middot is a horizontal separator. Stacked, the quote does not need it. */
  .sep { display: none; }
  .pull { display: block; margin-block-start: .05em; }
  .foot { display: block; }
  .note { margin-block-start: .5em; }
}


/* --------------------------------------------------------------------------
   14. Forced colours
   -------------------------------------------------------------------------- */

@media (forced-colors: active) {
  :focus-visible {
    outline: 3px solid Highlight;
    box-shadow: none;
  }
  body::after { display: none; }
  /* The marks, both states. Opacity survives forced colours, so the ink-in
     still happens; only the two inks it happens between are reassigned. */
  .b-dot, .s-fill { fill: CanvasText; }
  .b-moat, .s-knock, .s-moat { fill: Canvas; }
  .b-line, .s-line, .s-soft { stroke: CanvasText; }
  .b-moat, .s-moat { stroke: Canvas; }
}


/* --------------------------------------------------------------------------
   15. The right column: a portrait, and the index previewing itself into it

   The header becomes two columns wide enough to hold one, and the sheet grows
   by exactly the width of the new column plus its gutter. Everything below the
   header keeps its 32rem measure, so not one line of the index rewraps and the
   page is the same height it was: the fold budget is untouched by construction.

   In that column sits a single box. At rest it holds the portrait. Hovering or
   focusing a row of the index swaps in that row's preview. Both live absolutely
   inside a container of fixed size, so nothing here can move the page.

   No JavaScript. `:has()` lets the header react to a row below it, which is the
   only thing a script would have been for. With scripting off, the page is
   unchanged: portrait up, nine rows working.
   -------------------------------------------------------------------------- */

/* Off by default. Below the two-column breakpoint the column does not exist,
   so there is no portrait, no preview and no reserved space either. */
.hedbox { display: none; }

/* The panels live inside their rows now, not inside .hedbox, so hiding the
   portrait no longer hides them. Without this they render as ordinary blocks
   on a phone and print all nine descriptions into the list, which took the
   page from 844px to 5,392px. Hidden at the base, shown only where the rail
   exists. */
.pv { display: none; }

@media (min-width: 62rem) {
  :root {
    /* 32rem text + 2.5rem gutter + the rail + 2rem padding either side.
       The text column comes out at exactly the 32rem it has always been.

       The rail and the portrait are two different widths on purpose. The
       portrait is 132px because that is the size it reads at. The rail is
       wider because the preview text lives in it, and at 132px a two sentence
       blurb wraps to eight lines and starts hyphenating. */
    --measure: 52.75rem;
    --hed-w: 132px;
    --rail-w: 228px;
  }

  .head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--rail-w);
    column-gap: 2.5rem;
    align-items: start;
  }

  /* The sheet is wider now; the reading column is not. */
  .rule, .index, .foot { max-width: 32rem; }

  .hedbox {
    display: block;
    position: relative;
    width: var(--rail-w);
    /* Rail wide, portrait tall. The height follows the PORTRAIT's proportion,
       not the rail's: tying it to the rail with an aspect-ratio made the box
       279px instead of 161px and pushed the footnote below the fold on every
       laptop. Both children are absolutely positioned inside it, so neither
       the portrait nor the panel can change this height.

       The box reserves LESS height than the portrait draws. The portrait is
       161px tall and the text beside it is about 118, so reserving the full
       161 made the header 43px taller than it needs to be and put the footnote
       under the fold at 1100x700 and 1280x720. The rail is empty beside the
       first index rows, so the portrait simply hangs down into it. */
    height: 118px;
    /* The panel is taller than the portrait and hangs below the box into the
       empty column beside the first rows. Absolute, so it costs no height. */
    pointer-events: none;
  }

  /* The portrait. An alpha mask painted in the ink token, so it is the same
     near-black with a hue in it that the type is, and follows --ink if --ink
     ever moves. Nothing here is a colour value. */
  .hed {
    position: absolute;
    top: 0;
    left: 0;
    width: var(--hed-w);
    /* Its own height, not the box's, so it overhangs rather than squashing. */
    height: calc(var(--hed-w) * 538 / 440);
    background: var(--ink);
    -webkit-mask-image: url(/assets/illustrations/homepage/hedcut-v1.webp);
            mask-image: url(/assets/illustrations/homepage/hedcut-v1.webp);
    -webkit-mask-size: contain;
            mask-size: contain;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: top center;
            mask-position: top center;
  }

  /* Each panel now lives inside its own row rather than stacked in the
     header, so it opens level with the thing it describes. The right column is
     a stage: whichever row you are on takes it, at that row's height. */
  .item { position: relative; }

  .pv {
    display: block;
    position: absolute;
    top: -.35rem;
    left: calc(100% + 2.5rem);
    width: var(--rail-w);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }

  /* The last rows would hang their panel past the footer, so those few open
     upward from their own baseline instead of downward from their top. */
  .item:nth-last-child(-n+3) .pv { top: auto; bottom: -.35rem; }

  .pv-mark {
    display: block;
    width: 46px;
    height: 46px;
    color: var(--ink);
    /* The row mark is 1.44 device pixels of stroke at a 36px box. Redrawing it
       at 46 would take it to 1.84 and thicken the pen. These bring it back to
       1.61, a shade heavier than the row because it is a display size now, not
       a margin one. The second is the same weight pre-divided by the 2.41 that
       one of the nine is placed with. */
    --sw: 3.5;
    --sw-scaled: 1.45;
  }
  .pv-ico { display: block; width: 100%; height: 100%; }

  .pv-title {
    display: block;
    margin-block-start: .62rem;
    font-size: .92em;
    font-weight: 500;
    line-height: 1.2;
    letter-spacing: -0.004em;
    text-wrap: pretty;
  }

  /* The same hairline the section rule and the footer use, doing the same job
     a third time. One rule system on the page, still. */
  .pv-text {
    margin-block-start: .45rem;
    padding-block-start: .45rem;
    border-block-start: 1px solid var(--rule);
    color: var(--ink-soft);
    font-size: .745em;
    line-height: 1.46;
    text-wrap: pretty;
    /* 132px is a narrow measure by any standard. Hyphenation is what keeps it
       from tearing rivers through six lines of prose. */
    hyphens: auto;
    -webkit-hyphens: auto;
  }
}

/* The transitions are opt-in, the way the row marks and the reveal are. Reduced
   motion, or a stylesheet that only half-loads, lands on an instant swap that
   is still completely correct.

   Two channels. Opacity carries the cross-fade and holds for 120ms before it
   reverts, so sweeping the pointer down nine rows does not strobe the portrait
   back in between them. Visibility is the discrete one: it is what lets an
   outgoing panel leave the instant another row takes over, rather than fading
   out underneath the panel fading in. */
@media (min-width: 62rem) and (prefers-reduced-motion: no-preference) {
  .hed { transition: opacity 160ms var(--ease-out-quint) 120ms; }
  .pv {
    transition: opacity 160ms var(--ease-out-quint) 120ms,
                visibility 0s linear 280ms;
  }
}

/* Desktop only, and only where the two-column header actually exists. A touch
   device has no hover to preview from, and the row it would preview is the row
   the finger is already on. */
@media (hover: hover) and (pointer: fine) and (min-width: 62rem) {

  /* Any row engaged: the portrait goes at once, and comes back on the delay. */
  .sheet:has(.row:is(:hover, :focus-visible)) .hed {
    opacity: 0;
    transition-delay: 0ms;
  }

  /* Any row engaged: every panel that is not the chosen one gets out of the
     way immediately. The chosen one overrules this two lines later. */
  .sheet:has(.row:is(:hover, :focus-visible)) .pv {
    transition-delay: 0ms, 0ms;
  }

  /* One rule, not nine. The panel lives inside the row it belongs to, so the
     pairing is structural and cannot drift. This replaced nine nth-child hooks
     that matched a row to a panel by counting. */
  .item:has(.row:is(:hover, :focus-visible)) .pv {
    opacity: 1;
    visibility: visible;
    transition-delay: 0ms, 0ms;
  }
}

/* Forced colours. The mask is an alpha channel, so the portrait is whatever it
   is painted with, and CanvasText is the right answer there. */
@media (forced-colors: active) {
  .hed { background: CanvasText; }
}


/* --------------------------------------------------------------------------
   15. /about/

   The same paper, the same type, the same rail. This stylesheet is shared
   rather than duplicated: a reader arriving from the front page already has it
   cached, and the two pages cannot drift apart. The homepage-only rules
   (.index, .pv, .glyph) simply match nothing here.
   -------------------------------------------------------------------------- */

.page-about .sheet { align-content: start; }

/* The body centres a short page in the window. This one is prose and runs
   long, so it starts at the top and stays there. */
.page-about { align-content: start; padding-block: clamp(2rem, 6vh, 4.5rem); }

.backlink {
  font-size: var(--fs-meta);
  font-variant-caps: all-small-caps;
  letter-spacing: .055em;
  margin-block-end: .75rem;
}
.backlink a { color: var(--ink-faint); }
.backlink a:hover, .backlink a:focus-visible { color: var(--accent); }

/* The section label, in the same small caps the index metadata uses. */
.kicker {
  margin-block-start: .55rem;
  font-size: var(--fs-meta);
  font-variant-caps: all-small-caps;
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: .07em;
  color: var(--ink-faint);
}

.prose {
  max-width: 32rem;
  margin-block-start: var(--step);
}

.prose p {
  margin-block-end: .95em;
  line-height: 1.62;
  text-wrap: pretty;
}
.prose p:last-child { margin-block-end: 0; }

/* Touch target parity with the footer links. */
.backlink a { display: inline-block; min-height: 44px; line-height: 44px; }

@media (min-width: 62rem) {
  /* The prose column is the reading column, the same 32rem the index uses. */
  .page-about .rule, .page-about .prose, .page-about .foot { max-width: 32rem; }
}
