/* chrispenny.me — e-ink discipline, paper warmth.
   No webfonts (they cost a round trip and a render delay), no shadows,
   no gradients. Hairline rules and real typography do the work. */

:root {
  --paper:      #f6f3ec;
  --paper-sunk: #efebe1;
  --ink:        #17130f;
  --ink-soft:   #574f46;
  --ink-faint:  #8b8175;
  --rule:       #d9d2c4;
  --accent:     #9a4f21;

  /* Georgia sits early on purpose: it is present and well-hinted on Windows,
     which is what Chris reads on. The Mac faces come first only as an upgrade
     where they exist. "Sitka Text" was removed -- it is a Windows face but a
     distinctive bookish one, so it made the site look different on the one
     machine that matters most here. */
  --serif: Charter, "Iowan Old Style", Georgia, "Times New Roman", serif;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* TWO widths, because there are two jobs:
       --measure  the READING column -- body text and everything sitting in
                  the flow of it, pictures included. Must not grow past a
                  comfortable line length whatever the monitor does: it holds
                  ~65 characters at every breakpoint and the TYPE grows instead.
       --frame    the page frame -- masthead rule, footer, and the archive
                  list, whose date rail earns the extra width. Wider than the
                  measure, but not by much; it is a margin, not a canvas.
     Both are the SAME value here, so the phone layout this site was built
     around is exactly as it was; only the blocks at the end of this file
     pull them apart. */
  --measure: 34rem;
  --frame: 34rem;
  --gutter: 1.5rem;

  color-scheme: light dark;
}

/* Dark palette.
   Defined TWICE on purpose, and the guard matters:
     1. under prefers-color-scheme, excluded when the reader has explicitly
        chosen light -- so an explicit choice beats the OS in both directions;
     2. under an explicit data-theme="dark".
   No colour gets its ONLY definition inside a media query, so the toggle can
   always win. data-theme is stamped on <html> by the inline script in base.html
   before first paint, so there is no flash of the wrong theme. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:      #14120f;
    --paper-sunk: #1c1916;
    --ink:        #ece6db;
    --ink-soft:   #b3aa9c;
    --ink-faint:  #7d7365;
    --rule:       #332e28;
    --accent:     #d98a52;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --paper:      #14120f;
  --paper-sunk: #1c1916;
  --ink:        #ece6db;
  --ink-soft:   #b3aa9c;
  --ink-faint:  #7d7365;
  --rule:       #332e28;
  --accent:     #d98a52;
  color-scheme: dark;
}

:root[data-theme="light"] { color-scheme: light; }

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  padding: 0 var(--gutter);
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1.0625rem;
  line-height: 1.65;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: var(--gutter);
  top: 0.5rem;
  z-index: 10;
  background: var(--paper);
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--ink);
}

a { color: var(--accent); text-underline-offset: 0.16em; text-decoration-thickness: 1px; }
a:hover { color: var(--ink); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---------------------------------------------------------------- masthead */

.masthead {
  max-width: var(--frame);
  margin: 0 auto;
  padding: 2.25rem 0 1.25rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  align-items: baseline;
  justify-content: space-between;
  border-bottom: 1px solid var(--rule);
}

.wordmark {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: 0.02em;
  color: var(--ink);
  text-decoration: none;
}

.masthead nav {
  display: flex;
  gap: 1.1rem;
  font-family: var(--sans);
  font-size: 0.875rem;
}
.masthead nav a { color: var(--ink-soft); text-decoration: none; }
.masthead nav a:hover,
.masthead nav a[aria-current="page"] { color: var(--ink); text-decoration: underline; }

/* Theme toggle. Borderless so it reads as part of the nav rather than as a
   button competing with it. Hidden until the script marks it ready, so a
   no-JS reader is never shown a control that cannot do anything. */
.theme-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin-left: 0.1rem;
  background: none;
  border: 0;
  color: var(--ink-faint);
  cursor: pointer;
  line-height: 0;
}
.theme-toggle[data-ready] { display: inline-flex; }
.theme-toggle:hover { color: var(--ink); }
.theme-toggle .icon { display: none; }

/* Show the mode you will GET: moon while light, sun while dark. */
.theme-toggle .icon-moon { display: block; }

:root[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-sun  { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: none; }
  :root:not([data-theme="light"]) .theme-toggle .icon-sun  { display: block; }
}
/* An explicit light choice on a dark OS must still show the moon. */
:root[data-theme="light"] .theme-toggle .icon-moon { display: block; }
:root[data-theme="light"] .theme-toggle .icon-sun  { display: none; }

/* -------------------------------------------------------------------- main */

main {
  max-width: var(--frame);
  margin: 0 auto;
  padding: 2.5rem 0 1rem;
}

/* Prose keeps the reading measure and centres inside the frame. On a phone
   frame == measure, so this is a no-op; on a desktop it is what stops body
   text stretching to 90 characters a line just because the screen is wide. */
.prose {
  max-width: var(--measure);
  margin-left: auto;
  margin-right: auto;
}

.prose > :first-child { margin-top: 0; }

h1 {
  font-size: 1.85rem;
  line-height: 1.2;
  letter-spacing: -0.012em;
  margin: 0 0 0.75rem;
  font-weight: 600;
}

h2 {
  font-family: var(--sans);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 1rem;
}

h3 { font-size: 1.2rem; margin: 2rem 0 0.5rem; font-weight: 600; }

p, ul, ol, blockquote { margin: 0 0 1.35rem; }

.prose ul, .prose ol { padding-left: 1.2rem; }
.prose li { margin-bottom: 0.4rem; }

blockquote {
  margin-left: 0;
  padding-left: 1.1rem;
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
  font-style: italic;
}

hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0; }

img, video { max-width: 100%; height: auto; display: block; }

code, pre {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
}
pre {
  background: var(--paper-sunk);
  padding: 1rem;
  overflow-x: auto;
  border: 1px solid var(--rule);
}

.meta {
  font-family: var(--sans);
  font-size: 0.82rem;
  color: var(--ink-faint);
  margin: 0 0 1.75rem;
}

/* ------------------------------------------------------------------ streams */

.intro { margin-bottom: 3rem; }
.intro p:first-of-type { font-size: 1.2rem; line-height: 1.5; color: var(--ink); }

/* A stream sits at the reading measure by default, aligned with the prose
   above it -- a title/date row spread across the full frame leaves a canyon
   between the two. The ARCHIVE is the exception: it earns the extra width
   because its date rail uses it. */
.stream {
  max-width: var(--measure);
  margin: 0 auto 3rem;
}
.stream--wide { max-width: var(--frame); }

.entries { list-style: none; margin: 0; padding: 0; }
.entries li { margin: 0; border-top: 1px solid var(--rule); }
.entries li:last-child { border-bottom: 1px solid var(--rule); }

.entries > li > a {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 1rem;
  align-items: baseline;
  justify-content: space-between;
  padding: 0.85rem 0;
  text-decoration: none;
  color: var(--ink);
}
.entries > li > a:hover .entry-title { text-decoration: underline; text-decoration-color: var(--accent); }

.entry-title { font-weight: 500; }

.entries time,
.entries .handle {
  font-family: var(--sans);
  font-size: 0.8rem;
  color: var(--ink-faint);
  white-space: nowrap;
}

.entry-summary {
  margin: -0.35rem 0 0.9rem;
  font-size: 0.95rem;
  color: var(--ink-soft);
}

.empty { color: var(--ink-faint); font-style: italic; }

/* /videos/ — thumbnails on a grid that wraps itself: one column on a phone,
   more as the frame widens, no breakpoints needed. YouTube's hqdefault is 4:3
   with baked-in letterbox bars, so it is cropped to 16:9 to lose the bars. */
.video-grid {
  list-style: none; margin: 0 0 1rem; padding: 0;
  display: grid; gap: 1.5rem 1.25rem;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
}
.video-grid a { display: block; text-decoration: none; color: var(--ink); }
.video-grid img {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
  object-fit: cover; border: 1px solid var(--rule); margin-bottom: 0.5rem;
}
.video-grid .entry-title { display: block; line-height: 1.35; }
.video-grid a:hover .entry-title { text-decoration: underline; text-decoration-color: var(--accent); }
.video-grid time { font-family: var(--sans); font-size: 0.8rem; color: var(--ink-faint); }

/* Sits under the article, so it takes the article's width -- a rule spanning
   the whole frame beneath a measure-wide column reads as a mistake. */
.after {
  max-width: var(--measure);
  margin: 0 auto;
  font-family: var(--sans);
  font-size: 0.875rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}

/* The invitation to reply, at the foot of every post. Deliberately NOT a
   boxed call-to-action: a tinted panel with a button in it is the visual
   language of a newsletter signup, and it would be the loudest thing on a page
   whose whole point is the writing. This is one line of quiet sans, set the
   way the byline at the top is set, so it reads as the author speaking rather
   than as an ad slot. */
.reply {
  max-width: var(--measure);
  margin: 0 auto;
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
  font-family: var(--sans);
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--ink-soft);
}

.reply p { margin: 0; }

/* On the homepage the invitation sits directly under the intro, which has
   already made the invitation in words -- a rule between the two would cut the
   paragraph off from its own answer. */
.reply--intro {
  border-top: 0;
  padding-top: 0;
  margin: 1.25rem 0 0;
}

/* .after draws its own top rule. Following the reply block it would draw a
   SECOND one a couple of lines below the first, which looks like a bug. */
.reply + .after {
  border-top: 0;
  padding-top: 1.25rem;
}

.reach { color: var(--ink-soft); margin: 0 0 0.6rem; }
.reach a { color: var(--ink-soft); }
.reach a:hover { color: var(--accent); }

/* A page title above a FRAME-wide list has to align with that list, not with
   the reading measure, or the heading hangs indented over its own content. */
.page-head {
  max-width: var(--frame);
  margin: 0 auto 2rem;
}

/* ------------------------------------------------------------------ youtube */

.yt { margin: 0 0 1.5rem; }
.yt-play {
  display: block;
  position: relative;
  width: 100%;
  padding: 0;
  border: 1px solid var(--rule);
  background: var(--paper-sunk);
  cursor: pointer;
  aspect-ratio: 16 / 9;
  overflow: hidden;
}
.yt-play img { width: 100%; height: 100%; object-fit: cover; }
.yt-btn {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 2.5rem;
  color: var(--paper);
  text-shadow: 0 0 1.5rem rgba(0, 0, 0, 0.75);
}
.yt iframe { width: 100%; aspect-ratio: 16 / 9; border: 0; display: block; }

/* ------------------------------------------------------------------ figure */

.prose figure { margin: 1.75rem 0; }
.prose figure img { margin: 0; }
.prose figcaption {
  font-family: var(--sans);
  font-size: 0.8rem;
  color: var(--ink-faint);
  padding-top: 0.5rem;
}

/* -------------------------------------------------------------------- embed */

/* Third-party iframes (the Nomad List travel map). Fixed height because the
   provider does not post a resize message -- there is nothing to listen for. */
.embed {
  margin: 2rem 0;
  border: 1px solid var(--rule);
  background: var(--paper);
}
.embed iframe { width: 100%; height: 600px; border: 0; display: block; }
@media (max-width: 40rem) { .embed iframe { height: 420px; } }

/* Ported 2017 photos: give them air and a hairline so they read as plates
   against the paper rather than floating. */
.prose img { margin: 1.75rem auto; border: 1px solid var(--rule); }

/* ------------------------------------------------------------------- footer */

.footer {
  max-width: var(--frame);
  margin: 0 auto;
  padding: 2rem 0 3rem;
  border-top: 1px solid var(--rule);
  font-family: var(--sans);
  font-size: 0.85rem;
}

.elsewhere { display: flex; flex-wrap: wrap; gap: 0.6rem 1.25rem; margin-bottom: 1rem; }
.elsewhere a { color: var(--ink-soft); text-decoration: none; }
.elsewhere a:hover { color: var(--accent); text-decoration: underline; }

.fine { color: var(--ink-faint); margin: 0; }
.fine a { color: var(--ink-faint); }

/* ----------------------------------------------------------------- portrait */

/* Sits at the reading measure, exactly like any photograph inside a post.
   This was built at 14rem on the assumption of a head-and-shoulders crop, and
   the actual photograph is a wide two-person frame -- at 14rem both faces are
   too small to read, which defeats the only thing the picture is here to do.
   The slot was wrong for the picture, so the slot changed.
   max-width plus auto height still means a future portrait or square drops in
   without a special case; it would simply be taller. */
.portrait {
  margin: 0 0 2.25rem;
  max-width: var(--measure);
}

.portrait img {
  display: block;
  margin: 0;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
}

.portrait figcaption {
  font-family: var(--sans);
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--ink-faint);
  padding-top: 0.5rem;
}

/* ------------------------------------------------------------------ gallery */

/* One column at the reading measure, same as any photograph inside a post.
   A grid was the obvious thing and it is wrong here: these are photographs
   worth looking AT, and a two-up grid halves every one of them to save a
   reader some scrolling they came to do anyway. */
.gallery {
  max-width: var(--measure);
  margin: 0 auto 3rem;
}

.plate { margin: 0 0 3rem; }
.plate:last-child { margin-bottom: 0; }
.plate img { margin: 0; border: 1px solid var(--rule); }

.plate figcaption {
  font-family: var(--sans);
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink-soft);
  padding-top: 0.6rem;
}

/* Sits on its own line under the caption rather than trailing it: a link
   inline with the last sentence gets clicked by accident on a phone. */
.plate-source {
  display: block;
  margin-top: 0.3rem;
  color: var(--ink-faint);
  text-decoration: none;
}
.plate-source:hover { color: var(--accent); text-decoration: underline; }

/* ------------------------------------------------------------------ archive */

/* /writing/ is 51 entries spanning 2016-2026. Ungrouped that is a wall of
   near-identical rows; the year headings are the only structure the data
   already carries, so they are the ones to use. */
.year-group { margin: 0 0 2.75rem; }
.year-group:last-child { margin-bottom: 0; }

.year {
  font-family: var(--sans);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
  margin: 0 0 0.6rem;
}

/* ------------------------------------------------------------------ desktop */

@media (min-width: 40rem) {
  /* Pull the frame off the measure. Everything above this line already reads
     from the three tokens, so these blocks are the whole desktop layout. */
  /* 640px. gutter x2 leaves 36rem of room, so measure already fills it and
     there is genuinely no frame to split off yet -- claiming one here would
     be a width the page can never actually reach. Font and air only. */
  :root { --measure: 36rem; --frame: 36rem; --gutter: 2rem; }

  body { font-size: 1.125rem; }
  h1 { font-size: 2.25rem; }

  /* More air, now that there is room for it. */
  .masthead { padding: 2.75rem 0 1.5rem; }
  main { padding: 3.25rem 0 1.5rem; }
  .footer { padding: 2.5rem 0 3.5rem; }

  /* Date rail. The year lives in the group heading, so every date is the same
     width and a fixed column lines all 51 titles up on one edge -- instead of
     space-between scattering them across the row by title length. */
  .entries--archive > li > a {
    display: grid;
    grid-template-columns: 4.5rem 1fr;
    gap: 0 1.25rem;
    align-items: baseline;
  }
  .entries--archive > li > a time { grid-column: 1; grid-row: 1; }
  .entries--archive > li > a .entry-title { grid-column: 2; grid-row: 1; }
  .entries--archive .entry-summary { margin-left: 5.75rem; }
}

/* 832px -- the first width with room to spare, so this is where the frame
   actually splits off the measure. Waiting for 1024px left everything between
   a large tablet and a laptop on the phone layout, which is a third of the
   "it's still narrow" band.

   Photographs used to break OUT to the frame here. They no longer do: an
   image wider than the column it sits in breaks the left edge the reader is
   tracking down the page, and it makes the picture the loudest thing on a
   page that is supposed to be about the writing. Media stays on the measure
   with the text (Chris, 2026-09-20). Nothing takes --frame as a hard width
   now -- see the note in sites/check_widths.py. */
@media (min-width: 52rem) {
  :root { --measure: 37rem; --frame: 44rem; }
}

/* A laptop. The frame opens up properly and the TYPE grows with it -- that is
   the lever a width-only change misses. Widening the measure alone just runs
   the line past a comfortable length; bigger type at the SAME character count
   fills more of the screen and reads better, which is what "feels narrow"
   usually actually wants. The measure stays ~65 characters at every step. */
@media (min-width: 64rem) {
  :root { --measure: 39rem; --frame: 48rem; --gutter: 2.5rem; }

  body { font-size: 1.1875rem; }
  h1 { font-size: 2.6rem; }
  .masthead { padding: 3.25rem 0 1.75rem; }
  main { padding: 4rem 0 2rem; }
}

/* A real monitor (1440px and up). The frame stops at 52rem rather than filling
   the screen: with the media back on the measure, nothing between 52 and 85rem
   would have anything in it, and a masthead rule floating that far out past its
   own content reads as a mistake rather than as margin. */
@media (min-width: 90rem) {
  :root { --measure: 41rem; --frame: 52rem; }

  body { font-size: 1.25rem; }
  h1 { font-size: 2.9rem; }
}
