/* leebrooker.co.uk — one-page personal site.

   Design is Lee's own draft (reference/holding-page-draft-2026-08-24.jpeg),
   rebuilt rather than reinvented: same wordmark, same headline, same four
   numbered cards in the same asymmetric grid. Colours were sampled off that
   JPEG with System.Drawing rather than guessed by eye.

   The brand values themselves come from happy-days-parents-website/styles.css,
   which carries them from Lee's deck (2026-08-12): purple #7D1376, cyan
   #5CE0E6. The book cover confirms both. Where the sampled JPEG and the deck
   disagree by a hair (the draft's purple reads #7C1B7A) the deck wins, because
   the JPEG has been through lossy compression and the deck has not. */

/* Fonts are self-hosted, copied from the parents site. No fonts.gstatic.com
   request: that site removed it because it handed every visitor's IP to Google
   before consent, and a page reached from a QR code in a printed book should
   not reintroduce a third-party call for the sake of one typeface. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-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;
}
/* The metrics-matched fallback, and `size-adjust` is the whole point of it.
   At 100% it matched nothing. Inter is 8-12% wider than every face listed
   below, so text set in the fallback took up that much less width -- more than
   enough to change where a line breaks. Measured at 412px, the hero headline
   set on two lines in the fallback and three in Inter, so the moment Inter
   arrived everything beneath it dropped 39px. The About passage moved 26px for
   the same reason.

   That shift was invisible until the PostHog load was deferred, which is the
   only reason it is being fixed now. Before, the main thread was blocked long
   enough that the swap happened BEFORE first paint, so there was nothing drawn
   yet to shift. Making the page paint sooner uncovered a defect that had been
   latent since launch: it appeared once in 22 Lighthouse runs, at 0.116, on a
   site whose layout shift is documented as a perfect zero.

   THE BANDS ARE NOT DECORATION. A single size-adjust cannot do this, because
   what Inter needs rises with weight -- measured against Segoe UI at 107.7% for
   400, 108.7% for 500, 109.7% for 600, 110.7% for 700 and 111.9% for 800. This
   page uses every one of those: body at 400, nav at 500, kickers and headings
   at 700, the wordmark at 800. Setting one value tuned for body copy leaves the
   headline -- the largest type on the page and the thing that moves the most --
   still mismatched by 3%, which was exactly the element Lighthouse blamed.

   Three bands rather than five, and the reason is that more precision would be
   false. Which face `local()` actually resolves to depends on the device:
   Segoe UI on Windows, San Francisco on an iPhone, Roboto on Android, and
   these ratios were measured against the first. Being within a percent or two
   on all three beats being exact on the one nobody in this audience is using.

   None of this can touch the settled layout. The family is only used while
   Inter is loading, or if it never arrives at all. */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Segoe UI'), local('-apple-system'), local('Roboto'), local('Arial');
  font-weight: 100 450;
  size-adjust: 107.7%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}
@font-face {
  font-family: 'Inter Fallback';
  src: local('Segoe UI'), local('-apple-system'), local('Roboto'), local('Arial');
  font-weight: 451 650;
  size-adjust: 109.2%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}
@font-face {
  font-family: 'Inter Fallback';
  src: local('Segoe UI'), local('-apple-system'), local('Roboto'), local('Arial');
  font-weight: 651 900;
  size-adjust: 111.2%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

/* Lee's draft sets its display type in a neo-grotesque, not in Anton. Anton is
   the Happy Days display face and it is deliberately NOT used here: it is a
   condensed all-caps poster face, and the draft's headline is sentence case at
   a normal width. Using it would make this page look like the Happy Days site
   rather than like Lee's design. Inter carries both roles instead, tightened
   at display sizes, which is what --tight below is for. */
:root {
  --purple: #7d1376;
  --purple-dark: #5d0a57;
  --purple-tint: #f5edf5;

  /* Two cyans, and the difference is load-bearing. --cyan is the brand value
     and is a FILL: 1.58:1 on white, so it is unreadable as type on anything
     light. --cyan-card is the deeper shade Lee's designer used for card 04
     (sampled #42BDC4), which reaches 9.3:1 against --ink. That is why the
     cyan card below sets dark type and never white: white on --cyan-card is
     2.26:1 and fails AA outright. The book cover breaks this rule — it prints
     cyan type on white — so copy the identity from the cover, not its
     contrast decisions. Same rule the parents site follows. */
  --cyan: #5ce0e6;
  --cyan-card: #42bdc4;

  --ink: #171717;
  --paper: #eae5df;
  --cream: #f3f2ee;
  --white: #ffffff;
  /* #6d6d6d until the contrast sweep. It reads as a safe grey because it is
     5.17:1 on white -- but nothing on this site sets it on white. It is set on
     --paper, where the same grey is 4.13:1 and fails. Four points darker clears
     4.5:1 on every light ground here at once: 4.59 on --paper, 5.13 on --cream,
     5.74 on white. Judge this token against --paper, which is the darkest of
     the three, and it holds everywhere. */
  --muted: #666666;
  --line: #dad4cc;

  --tight: -0.03em;
  --shell: 1360px;
  --gutter: 20px;

  /* One height for every photograph at the foot of a card. Held here rather
     than written out three times because the cards are all the same width now,
     so three photographs of three different heights read as a mistake rather
     than as a rhyme -- and separate values drift the moment one is tuned. */
  --shot-h: clamp(150px, 17.5vw, 250px);
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

/* `scroll-padding-top` without `scroll-behavior: smooth`, and both halves of
   that are deliberate.

   The header is sticky, so an anchor jump lands the target under it unless
   something offsets it: measured at 13px of the section kicker swallowed on a
   1280px laptop. That is what the padding fixes, and it is still doing that
   work for any jump the script below does not handle.

   `scroll-behavior: smooth` is absent because it hands the duration to the
   browser and Chrome's is distance-based: a hero-to-contact jump measured
   1004ms, long enough that a wheel nudge partway through fights it for the
   scroll position. The inline script at the end of index.html animates the
   same jump at a fixed 340ms instead. Do not add the CSS property back --
   it is the version that was already rejected, and with the script present
   the two would compete for the same scroll. */
html { scroll-padding-top: 100px; }
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

html, body { margin: 0; padding: 0; }
body {
  font-family: 'Inter', 'Inter Fallback', system-ui, -apple-system, Segoe UI, sans-serif;
  background: var(--paper);
  color: var(--ink);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* `text-rendering: optimizeLegibility` was here and is deliberately gone.
     It forces kerning and optional ligatures on for every glyph run in the
     document, which has to be resolved during the first layout -- the one that
     happens on a cold cache, on a phone, while somebody stands there having
     just scanned a printed code. Measured over interleaved runs at 4x CPU
     throttle it was the second largest single cost on the page.

     It buys nothing here. Chrome applies kerning under `auto` anyway for text
     at these sizes, and Inter's own metrics carry the pair adjustments that
     matter. The keyword is a hint from an era when kerning was off by default.

     Removed on evidence rather than on reputation: every width was re-rendered
     and compared, and not one line breaks in a different place. */
}
img { max-width: 100%; display: block; }

/* Focus is visible on every ground the page uses. The default UA ring is a
   thin dark outline, which disappears entirely on the black and purple cards
   where several of the links live. */
:focus-visible {
  outline: 3px solid var(--cyan);
  outline-offset: 3px;
  border-radius: 2px;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 var(--gutter);
}
@media (min-width: 720px) { :root { --gutter: 40px; } }
/* A third step, because --shell is 1360 and a 1280 laptop is under it: the
   shell stops being centred with space around it and becomes the full width of
   the screen, so 40px is all that stands between the text and the bezel. At
   1440 the same 40px sits inside another 40px of leftover margin and reads
   fine, which is why this only shows up on the smaller desktop widths. Must
   come after the rule above -- same custom property, same specificity. */
@media (min-width: 1000px) { :root { --gutter: 64px; } }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--purple);
  color: var(--white);
  padding: 12px 20px;
  font-weight: 600;
  z-index: 100;
}
.skip:focus { left: 0; }

/* The wordmark's scroll target, clipped rather than hidden so it keeps an
   accessible name -- see the comment on the element itself. Absolutely
   positioned, so it takes its static position at the document origin and still
   reports `-scrollY`, which is the whole reason the scroll arithmetic works. */
#top {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- Header ---------- */

.site-header {
  background: var(--cream);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 50;
}
.site-header .shell {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 68px;
}
@media (min-width: 720px) { .site-header .shell { min-height: 84px; } }

/* 8px vertical takes the target from 29px to 45px. It was under the floor
   before as well, and mattered less while this was a link to the page you were
   already on; it is now the only route back to the top of a page that is many
   screens long on a phone, so it has to be tappable. Vertical only, and the
   header's 68px minimum already clears 45, so nothing above moves.

   `display: inline-block` is doing nothing at present and is deliberate all
   the same: the wordmark is a flex child, so it is blockified and the padding
   counts either way. It is there for the case where this rule is reused
   outside a flex parent, where an inline element's vertical padding does not
   grow the line box and the target would silently drop back to 29px. */
.wordmark {
  font-size: clamp(19px, 2.6vw, 24px);
  font-weight: 800;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  display: inline-block;
  padding: 8px 0;
}
.wordmark span { color: var(--purple); font-weight: 500; }

.site-nav { display: flex; align-items: center; gap: clamp(18px, 2.6vw, 34px); }

/* The three text anchors are desktop-only, and there is deliberately no
   hamburger behind them.

   The old note here justified that by the page being "four thumb-scrolls end to
   end", which stopped being true once the photographs and the community band
   went in -- it is several times that now. A measured height is exactly the
   kind of claim that goes stale between merges, so the decision deliberately
   does not rest on one; it stands on the other two reasons.

   A hamburger here would open onto three in-page anchors, and all three point
   at content the reader reaches by continuing to scroll -- a menu whose whole
   answer is "the page has three sections below this one". The one destination
   that is not simply further down the same scroll is the form, and that is the
   button, which stays at every width because it is the one thing a QR-code
   visitor is here to press.

   The second reason is the one that would be expensive to get back. A
   hamburger needs script or a checkbox hack, and every script on this page is
   optional to it by design. A menu that fails to open is a worse header than
   no menu. */
.site-nav a:not(.btn) { display: none; }
@media (min-width: 900px) {
  .site-nav a:not(.btn) {
    display: inline;
    color: var(--ink);
    text-decoration: none;
    font-size: 16px;
    font-weight: 500;
  }
  .site-nav a:not(.btn):hover { color: var(--purple); }
}

.btn {
  display: inline-block;
  background: var(--purple);
  color: var(--white);
  text-decoration: none;
  font-weight: 600;
  font-size: clamp(14px, 1.6vw, 16px);
  padding: 13px clamp(16px, 2.4vw, 28px);
  border: 0;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s ease;
}
.btn:hover { background: var(--purple-dark); color: var(--white); }

/* 48px minimum height, not 44px: the thumb target guidance is a floor, and the
   primary action on a page reached by scanning a code with a phone should sit
   above it rather than on it. */
.btn--lg {
  font-size: clamp(15px, 1.7vw, 17px);
  padding: 15px clamp(22px, 2.6vw, 32px);
  min-height: 48px;
  display: inline-flex;
  align-items: center;
}

/* The quieter half of the pair. Underlined rather than boxed, so there is
   never a moment where two things look equally like the primary action. */
.btn-ghost {
  font-weight: 600;
  font-size: clamp(15px, 1.7vw, 17px);
  color: var(--purple);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  /* 11px rather than 6px vertical, which takes the target from 37px to 47px on
     a phone. It was under the 44px floor throughout, and that mattered less
     while this was the alternative action; it now carries the podcast, which is
     the only route to the podcast from the first screen. Vertical only: the
     2px horizontal keeps the underline the width of the words rather than
     floating clear of them at both ends. */
  padding: 11px 2px;
  transition: border-color 0.15s ease;
}
.btn-ghost:hover { border-bottom-color: var(--purple); }
.btn-ghost::after { content: ' \2193'; }

/* ---------- Hero ---------- */

.hero { padding: clamp(36px, 5vw, 60px) 0 clamp(32px, 4vw, 48px); }

/* Mobile-first: one column, copy then portrait. The two-column split arrives
   at 900px along with everything else on the page. */
.hero-grid { display: grid; gap: clamp(24px, 3vw, 40px); }

/* The portrait comes last on a phone, and the book flag is the reason. Measured
   at 390x664: with the photograph first it took the top 220px of the screen, the
   primary button ended at 661 of 664 and the flag ran 689-774 -- entirely below
   the fold. So the one-second confirmation that a scanned code has landed in the
   right place was not on the screen the code is scanned with. Moved last, the
   eyebrow, headline, lede, both buttons and the flag all fit, and the portrait
   still shows from 554 so it reads as something to scroll to.

   It stays flush left rather than centring. Everything else in the hero starts
   at the shell gutter, including the flag's cyan rule, and a centred square is
   the only element on the page off that line.

   Scoped to a max-width query rather than done with `order` alone: above 900px
   both children take explicit `grid-column` values, and reordering them there
   pushes the portrait onto a second row under sparse auto-placement. */
@media (max-width: 899px) {
  .hero-media { order: 2; }
  .hero-copy { order: 1; }
}

.eyebrow {
  font-size: clamp(12px, 1.4vw, 14px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--purple);
  /* 26px, not 18px. The h1 below runs at line-height 0.98, so at 92px its
     glyphs are taller than their own line box and the ascenders overflow
     upwards into whatever sits above. At 18px they were striking through this
     line. The gap has to clear the overflow, not just look like a gap. */
  margin: 0 0 26px;
  line-height: 1.5;
}
/* Flex rather than inline text, so the two credentials wrap as whole units.
   As loose text it broke wherever the width ran out -- at 1200px that left
   "HOST" stranded at the end of line one, away from the podcast it names.
   Each part is nowrap, so the only break available is between them, and the
   separator trails line one where it reads as "continued below" rather than
   as a stray bullet at the head of line two. */
.eyebrow { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 8px; }
.eyebrow-part { white-space: nowrap; }
/* An explicit colour rather than `opacity: 0.45`, which painted #b987b0 on
   --paper at 2.34:1. Lighthouse does not report this one -- axe skips text
   nodes made only of punctuation -- so it came out of the sweep over the
   rendered document instead, and it is the same defect as the card numbers:
   an opacity hiding the colour that is really on screen.

   It is aria-hidden and decorative, so it is arguably exempt. Fixed anyway,
   because a separator too faint to see is not separating anything, and that is
   its whole job: it marks where "Founder of Happy Days Creative" ends and
   "Host of The School Photo Podcast" begins. 4.51:1, which is as quiet as it
   is allowed to be. */
.eyebrow-dot { color: #994991; }

/* The page had no typographic level between the 92px headline and 17px body,
   so the two read as unrelated objects rather than one block. This is that
   level. */
.hero-lede {
  max-width: 40ch;
  font-size: clamp(18px, 2.2vw, 23px);
  line-height: 1.45;
  color: var(--ink);
  margin: 0 0 clamp(24px, 3vw, 32px);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(12px, 1.6vw, 20px);
  margin-bottom: clamp(28px, 3.4vw, 40px);
}

.hero h1 {
  margin: 0 0 clamp(20px, 3vw, 32px);
  font-size: clamp(40px, 8.4vw, 92px);
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: var(--tight);
  color: var(--ink);
}

/* Laptop screens are wide and short, and the clamps only ever asked about
   width. On a 1280x585 viewport the hero measured 642px before the buttons,
   so the first screen carried no action and no book flag at all -- the two
   things this page exists to deliver. Keyed to height so ordinary displays
   keep Lee's headline at full size; only the short screens tighten. */
/* The spacing here is what pays for the headline size set at the end of this
   file. Growing the headline from 60px back to 76px costs 31px of first screen,
   and the book flag had only 12px of slack before it dropped under the fold, so
   every value below was tightened to buy that back. Measured at 1280x585: the
   flag ends at 574 of 585, which is the slack it had before.

   `.hero-lede` used to be in this block and is not any more. It was declared
   `font-size: 19px; margin-bottom: 22px` here and did neither, because
   `.hero-lede` is declared again at ~373 with a `margin` shorthand -- later in
   the file, and a media query adds no specificity. It now lives in the block at
   the end, which is the only place in this file that cannot be beaten. */
@media (min-width: 900px) and (max-height: 720px) {
  /* Every gap in the hero, trimmed as far as it goes without touching the
     headline. The flag ran to 633px against a 585px fold on this laptop, so
     the thank-you and the rank were off the first screen. The headline is the
     one thing not cut: 76px here is a decision already made twice, once
     against Lee's own preference read in physical pixels, and it is not worth
     reopening for 23px of scroll.

     The flag lands at 583 against the 585 fold with the two-line measure doing
     most of the work, so this padding buys the clearance back up to six pixels.
     Two was too fine a margin to leave a first-screen guarantee standing on. */
  .hero { padding-top: 16px; }
  .hero h1 { margin-bottom: 10px; }
  .hero .eyebrow { margin-bottom: 8px; }
  .hero-actions { margin-bottom: 14px; }
  .hero .book-flag { padding: 10px 16px 10px 12px; }
}

/* Same problem on a short phone, where it matters more: at 390x664 the primary
   button was cut off by the bottom edge, so the first screen of a page reached
   by scanning a QR code offered nothing to press. Only the spacing moves --
   the portrait stays a portrait rather than shrinking back to an avatar.

   `.portrait { max-width: 190px }` used to be in this block and was never
   applied: `.portrait` was declared again below with its own `max-width`, later
   in the file, and a media query adds no specificity. It rendered at 220px for
   as long as it existed here -- the fourth rule this stylesheet has lost to
   source order. It is gone rather than moved: the portrait now runs the full
   column width on every phone, so there is no cap left to lose. */
@media (max-width: 899px) and (max-height: 740px) {
  .hero { padding-top: 28px; }
  .hero-grid { gap: 20px; }
  .eyebrow { margin-bottom: 18px; }
}

/* Above 900px the hero is two columns, and the point of it is the empty space
   it removes. Stacked, the headline sat top-right and the intro bottom-left,
   which left the top-left and bottom-right quadrants empty and pushed the
   first card 806px down a 900px-tall viewport -- the cards were below the fold
   on a laptop. Side by side and bottom-aligned, the same three elements read
   as one block. Lee's draft keeps its right-aligned headline either way.

   `align-items: end` is what does the work: the intro and the cyan flag sit on
   the headline's baseline rather than floating at the top of a tall column. */
@media (min-width: 900px) {
  .hero-grid {
    grid-template-columns: minmax(0, 0.62fr) minmax(0, 1fr);
    column-gap: clamp(36px, 5vw, 72px);
    /* `center`, not `start`. The portrait is a 444px square and the copy beside
       it runs 637, so top-aligned the left column ended at 589 while the right
       ran to 782 -- 193px of empty gutter under the picture, along the bottom
       edge of the first screen, which is what made the hero read as weighted to
       the right. Centred it splits into 96px above and 96px below, and the
       column that is genuinely taller stops announcing it at one end only. */
    align-items: center;
  }
  .hero-media { grid-column: 1; }
  .hero-copy { grid-column: 2; }
  /* Left, not right. Right-aligned is Lee's own and it is the signature of his
     draft, so this is a departure and it is deliberate: in the draft the
     headline stands alone in its column, and here it sits above three
     left-aligned blocks.

     Measured at 1440px, the right-aligned headline was 521px wide in a 746px
     column, so it floated 225px in from the left while the eyebrow, intro and
     buttons all started flush left -- two ragged edges pointing opposite ways.
     Sharing one left edge takes that spread from 225px to 0. It hid on a
     1280x585 laptop only because the short-viewport rule drops the headline to
     76px, which fills 689px of a 701px column and lands almost flush by
     accident.

     One line to revert if Lee wants his alignment back. */
  .hero h1 { text-align: left; }
}

/* The portrait is square rather than circular because every other edge on the
   page is square -- the cards, the button, the social chips. A single round
   element would read as borrowed from a different design.

   It also sits in the hero's left column, which is the emptiest part of the
   composition, so it fills space the layout already had rather than asking for
   more. The photograph's own backdrop is a dark slate, close enough to --ink
   that it needs no frame. */
/* Was clamped to 136px, which is avatar size: too small to be a portrait and
   too big to be an icon, floating above the text with nothing anchoring it.
   At full column width it becomes the thing it should always have been on a
   personal site, which is a photograph of the person.

   No cap on a phone either, and the reason the old 220px one existed has gone.
   It was there to stop the photograph eating the first screen, which ordering
   it last already does. What the cap did instead was leave a small square in a
   wide empty band with a ragged right edge, when every other element in the
   hero -- the headline, the lede, the book flag -- runs gutter to gutter. Full
   width puts it on the same two edges as the flag directly above it.

   The cost is sharpness on a 3x phone: the file is 800x800 and this renders at
   about 372 CSS pixels there, so it is upscaled roughly as far as it already is
   on a 2x laptop. Re-encoding at 1200 is the fix if it ever shows. */
.portrait {
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  margin: 0;
}
/* The tablet band used to cap the portrait at 430px here. It now shares one
   measure with the lede and the book flag, and that rule is at the END OF THE
   FILE -- `.hero-lede` is declared again at ~499 with its own `max-width`, so
   an override placed here would have been beaten on source order exactly as
   three others already were. See "The tablet measure" at the foot of this
   stylesheet. */
/* Square on desktop too, not 4/5. The source file is 320x320, so a 4/5 box was
   cropping the sides off a photograph that has none to spare and adding 108px
   of height for the privilege: the portrait ran 543px on a 585px laptop
   viewport and finished 60px below the text column it sits beside, which is
   what made the hero taller than the screen. Same width, same crop as the
   original, 108px shorter. */
@media (min-width: 900px) {
  .portrait { aspect-ratio: 1; }
}

.hero-lede {
  max-width: 46ch;
  font-size: clamp(17px, 2vw, 20px);
  color: var(--ink);
  margin: 0 0 20px;
}

/* The one line on the page written for a reader holding the book: they have
   scanned a code printed on paper and need to know inside a second that they
   have arrived at the right place. Cyan as a fill, purple for the type. */
/* This line does more work than anything else on the page: it is what tells
   somebody holding a printed book that the code they just scanned went where
   it was meant to. It was a flat cyan block with small text in it, which read
   as a highlighter accident. Now it carries a thumbnail of the actual cover,
   so the confirmation is visual before it is verbal -- they match the object
   in their hand against the picture without reading a word. */
.book-flag {
  display: flex;
  align-items: center;
  gap: 16px;
  background: var(--white);
  border-left: 5px solid var(--cyan);
  box-shadow: 0 2px 14px rgba(23, 23, 23, 0.07);
  color: var(--ink);
  padding: 14px 20px 14px 16px;
  font-size: clamp(14px, 1.6vw, 16px);
  font-weight: 500;
  line-height: 1.45;
  /* 60ch, not 52. The thank-you made this a three-line note, and the third
     line is what put its foot at 597px against a 585px fold on a 1280x585
     laptop. At 60ch the same sentence sets on two lines and clears it. The
     measure is generous for body copy and right for this: it is one short
     note in a box, not a paragraph to be read down. */
  max-width: 60ch;
  margin: 0;
}
.book-flag img {
  width: 84px;
  height: 56px;
  object-fit: cover;
  flex: none;
}
.book-flag strong { font-weight: 700; }

/* The thumbnail used to be hidden below 420px, which removed it from the one
   place it earns its keep. Almost every arrival scanned a code printed in the
   book and is holding it on a phone: the small screen is where "does the
   picture match the object in my hand" does the most work, and it was the only
   screen not getting it. Shrunk rather than dropped -- 64px still reads as the
   cover at arm's length, and the file is already in cache from the book card
   below, so it costs no request. */
@media (max-width: 420px) {
  .book-flag { gap: 12px; padding: 12px 14px 12px 12px; }
  .book-flag img { width: 64px; height: 44px; }
}

/* ---------- Section furniture ---------- */

.kicker {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--purple);
  margin: 0 0 clamp(16px, 2.2vw, 24px);
}

.section { padding: clamp(36px, 5vw, 56px) 0; }

/* 72px above and below each section is generous on a 900px-tall display and
   wasteful on a 585px one, where it costs a quarter of a screen twice over.
   This block has to sit after the rule it overrides: a media query adds no
   specificity, so the same selector earlier in the file loses on source order
   and the override silently does nothing. */
@media (min-width: 900px) and (max-height: 720px) {
  .section { padding: 48px 0; }
}

/* The hero and the cards both sat on --paper, so the top two-thirds of the
   page read as one unbroken beige field with some boxes floating in it. The
   card section moves to --cream, the same ground as the header, which gives
   the page three bands instead of one and ties the top and middle together.
   The two greys are four points apart, so it reads as a change of surface
   rather than a change of colour. */
#work {
  background: var(--cream);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

/* ---------- The community band ---------- */

/* --paper, which is the hero's ground. It gives the page paper, white, cream,
   paper, ink rather than introducing a fifth surface for one section, and it
   is the only ground already on the page that this band could take without
   sitting on the same colour as the cards above it. */
.community { background: var(--paper); }

/* Text left, artwork right, and it only goes side by side from 1200px. Below
   that it stacks, for the reason the About section is stacked at the same
   width and measured rather than guessed: the text reflows and the picture
   does not, so a narrower band makes the text taller and the picture shorter
   at the same time. Built at 5fr/7fr first and measured at 1024, the text ran
   772px against the artwork's 209 -- a picture floating in the middle of a
   column three times its height. Splitting evenly does not fix that either;
   only stopping the two-column layout before it gets there does.

   The measure widens to 58ch here rather than the 46 the rest of the page
   uses. At 46 the paragraph was 430px wide inside a 493px column and ran nine
   lines, which is what made the column tall in the first place.

   `align-items: center` because the two sides still cannot be made to finish
   together at 1440. Centred, the remainder reads as a picture beside a
   paragraph rather than as one column running short. */
.community-grid { display: grid; gap: clamp(24px, 3.4vw, 48px); align-items: center; }
.community-copy { font-size: 17px; line-height: 1.6; margin: 0 0 28px; max-width: 58ch; }
.community-art { width: 100%; height: auto; }

@media (min-width: 1200px) {
  .community-grid { grid-template-columns: minmax(0, 6fr) minmax(0, 6fr); }
}

/* ---------- About ---------- */

/* A band of its own, and white rather than --paper for the reason the card
   band exists further down: left on the hero's ground, the hero and 350 words
   of bio would read as one unbroken beige field about 1400px tall, which is
   the exact fault --cream was introduced to fix. White is also the right
   ground for photographs, and this is the first section to carry any. */
.about {
  background: var(--white);
  border-top: 1px solid var(--line);
}

.about-grid { display: grid; gap: clamp(24px, 3.2vw, 44px); }

/* These two must stay ABOVE the media query below, and it is not a matter of
   taste. A media query adds no specificity, so `.about-media { display: grid }`
   sitting later in the file beat `display: contents` inside the query on source
   order alone -- no error, no warning. The wrapper stayed a one-column grid,
   the column placements meant for the page grid were applied inside it instead,
   and both photographs rendered about 80px wide. The same trap already cost
   this stylesheet a short-viewport rule that did nothing. */
.about-media { display: grid; gap: clamp(12px, 1.6vw, 18px); }
.about-media img { width: 100%; height: auto; }

@media (min-width: 900px) {
  /* Both photographs in one column beside the bio. A staggered diagonal was
     tried -- Handbook top right, Mustang bottom left -- and it works, but it
     leaves white in the two counter-corners rather than in one place, and 350
     words makes a tall enough column that the patch beside the lower half of
     the bio ran to about 660px. One column keeps the white in a single block
     where it reads as the text column simply being longer than the pictures.

     Stretching an image to fill that leftover height was also tried and
     rejected, and that one was a real fault rather than a preference: the text
     column reflows with width and the pictures do not, so at 1280 the Mustang
     was handed a 511x598 box -- taller than it is wide -- and `object-fit:
     cover` answered by cropping a landscape photograph of a car down to a
     narrow vertical slice through the middle of it. A cropped or soft
     photograph on a photographer's site costs more than white space does. The
     pictures keep their own shape; 1.15/1 rather than 1.25/1 brings the two
     columns closer to finishing together without touching either. */
  .about-grid {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    align-items: start;
  }
  /* The heading is a row of its own above both columns. Without this it is a
     grid item like the other two and takes the first column, pushing the bio
     into the second and the pictures onto a row below. */
  .about-head { grid-column: 1 / -1; }
}
/* The grid gap is now what separates the heading from the two columns, so the
   heading's own bottom margin would double it -- 44 plus 20 at 1440. Written
   with three classes rather than moved down the file because `.about h2` sets
   that margin further on and equal specificity would let it win on source
   order, which is the trap this stylesheet has lost five rules to. */
.about-grid .about-head h2 { margin-bottom: 0; }

/* One column again between 900 and 1200, because two columns cannot balance at
   those widths. Both sides move the wrong way together: the media column
   narrows so the pictures shrink, and the text column narrows so the same 350
   words get taller. Measured against the foot of the bio, the pictures finished
   138px short at 1440 and 611px short at 1024.

   Moving the divider does not fix it, and that was tried rather than assumed:
   an even 1fr/1fr split at 1024 closed 611px of white down to 589px, because
   the 30px each picture gained came straight out of the text column and the bio
   grew by the same amount it saved.

   So the pictures come underneath instead, two abreast with the banner across
   both, which is what the width is actually good for. Above 1200 the 1.15/1
   split stands untouched.

   After the rule it overrides. A media query adds no specificity and this
   stylesheet has already lost four rules to source order. */
@media (min-width: 900px) and (max-width: 1199px) {
  .about-grid { grid-template-columns: minmax(0, 1fr); }
  .about-media { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The banner is 2.40:1 against the photographs' 1.5:1, so it takes the full
     row rather than sitting as an odd third in a two-up grid. It is also the
     one image here with type in it, and this is the width where that type
     finally has the room to be read. */
}

/* One heading level shared by all three sections, so About, What I do and
   Contact set their titles identically. They were drifting -- two of them had a
   heading at all, at slightly different margins, and the third had none. The
   cards keep their own smaller `.card h2`; this deliberately does not reach
   them. */
.about h2,
.section-title {
  font-size: clamp(30px, 5vw, 56px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: var(--tight);
  margin: 0 0 20px;
  color: var(--ink);
}

/* The cards need more air beneath this than a paragraph does. */
#work .section-title { margin-bottom: clamp(22px, 2.8vw, 32px); }

/* Smaller than the other section headings, and this is the one place a heading
   is measured against a picture rather than against a column. Lee's is six
   words where the others are two or three, so at 56px it took three lines and
   190px of a column already 328px taller than the artwork beside it. At 40px
   it takes two and the band comes closer to balancing. After the shared rule
   because a later selector is the only thing that beats it -- placed earlier
   it would lose on source order and do nothing, which has happened five times
   in this file. */
#community .section-title { font-size: clamp(28px, 3.4vw, 40px); }

/* 62 characters, against the 46 the cards use. This is the only long-form
   reading on the site -- around 350 words where a card carries two sentences --
   and a measure tuned for a card paragraph would run it to half again as many
   lines for no gain. */
/* `:not(.kicker)` is load-bearing, the same way `.contact a:not(.btn)` is. The
   kicker is a `<p>`, so `.about-copy p` outranks `.kicker` by one element and
   was repainting it at 18px, bold, with letter-spacing -- against 13px for the
   identical kicker in "What I do", which sits in no such wrapper. Three section
   kickers, two of them silently a third larger than the third. */
/* The three links inside his closing sentence. There is no global anchor rule
   in this stylesheet -- every link so far has been a button, a card CTA or a
   list item with its own colour -- so without this they would render in the
   browser's default blue, the one colour nowhere else on the page. Purple on
   white is 8.6:1, and the underline is permanent rather than on hover because
   these sit inside a paragraph and nothing else marks them as links. */
.about-copy a {
  color: var(--purple);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  font-weight: 600;
}
.about-copy a:hover { text-decoration-thickness: 2px; }

.about-copy p:not(.kicker) {
  max-width: 62ch;
  font-size: clamp(16px, 1.8vw, 18px);
  line-height: 1.65;
  margin: 0 0 20px;
}

/* One sentence lifted a level. It is the line that says what the site is for,
   and sitting seventh in a run of seven visually identical paragraphs it was
   invisible. Purple type on white, never cyan: --cyan is 1.58:1 here and is a
   fill only, which is why it draws the rule and not the words.

   Selector carries `.about-copy` rather than an `!important` so it simply
   outranks the paragraph rule above on specificity. */
.about-copy p.about-pull {
  max-width: 30ch;
  font-size: clamp(20px, 2.4vw, 27px);
  line-height: 1.35;
  font-weight: 600;
  letter-spacing: var(--tight);
  color: var(--purple);
  border-left: 4px solid var(--cyan);
  padding-left: clamp(16px, 2vw, 24px);
  margin: clamp(26px, 3vw, 36px) 0;
}

/* His sign-off, and it closes the section the way he closes a message. */
.about-copy p.about-signoff {
  font-weight: 700;
  color: var(--purple);
  margin-bottom: 0;
}


/* ---------- Card grid ---------- */

/* Lee's draft alternates the column split: row one is roughly 59/41, row two
   41/59. Measured off the screenshot at 1600px wide — purple 780px against
   black 545px, then white 555px against cyan 775px. 1.4fr/1fr reproduces it
   within a few pixels. */
/* One grid for all four, not one per row. A grid can only equalise a card
   against the others on its own row, so two grids left row two 115px taller
   than row one -- set by the book card's title and cover, against a podcast
   card that had no say in it.

   The earlier note here said a single grid could not vary its track sizes from
   row to row, which is true of a two-column grid and false of a twelve-column
   one. Spanning 7 then 5, and 5 then 7, reproduces the draft's 1.4:1 flip to
   within three pixels of the old `1.4fr 1fr`, and `grid-auto-rows: 1fr` then
   gives all four cards one height. */
/* 24px, matching `--card-pad` at this width rather than the 14px the old
   clamp bottomed out at. Stacked, every gap is a full-width band between two
   solid grounds, and three of the four have a full-bleed photograph on the
   upper side -- so the space between two cards was reading as narrower than
   the space inside one card's own edge, which made the boundary a seam rather
   than a separation. Rendered at 14, 24 and 32: 32 opens a hole against the
   same 24px padding, so the gap matching the padding is the one that reads as
   two objects. */
.cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
}

@media (min-width: 900px) {
  .cards {
    grid-template-columns: repeat(12, 1fr);
    /* Side by side with cream around all four edges, a card no longer needs
       the gap to out-measure its padding. */
    gap: clamp(16px, 1.6vw, 20px);
    /* `auto`, not the `1fr` that was here. `1fr` gave all four cards one
       height, which meant every card matched the tallest anywhere in the
       section rather than the tallest beside it -- and the tallest is the
       speaking card, which is full. Row one carries two cards with less to say,
       so it was inheriting 138px and 170px of empty colour it had no way to
       fill. Each row now sizes to its own content and the two cards within a
       row still stretch to each other, which is the part that has to hold: a
       card standing shorter than the one next to it is the fault the single
       grid was introduced to fix, and rows of different heights is not that. */
    grid-auto-rows: auto;
  }
  /* Four equal cards. This is a departure from Lee's draft, which alternates
     the split -- roughly 59/41 then 41/59, measured off his screenshot at
     1600px as purple 780 against black 545, then white 555 against cyan 775.
     `text-align` on `.hero h1` and these four lines are the two places his
     design was overruled; both revert in one edit. */
  .cards > .card:nth-child(1) { grid-column: 1 / span 6; }
  .cards > .card:nth-child(2) { grid-column: 7 / span 6; }
  .cards > .card:nth-child(3) { grid-column: 1 / span 6; }
  .cards > .card:nth-child(4) { grid-column: 7 / span 6; }

  /* The two wide cards hold the least type and now sit in the same box as the
     book card, which carries a photograph. Measured at 29% and 44% full, the
     Happy Days card with a single 223px void in the middle of it.

     Both changes are the same idea: give the words the room instead of leaving
     it empty. A wider card gets a larger heading, which is ordinary editorial
     scaling rather than a patch, and a shorter measure -- 34 characters against
     46 -- turns a two-line paragraph into three or four. A 46ch line on a 692px
     card is also simply too long to read comfortably; the narrower measure is
     what the typography wanted anyway. */
  /* The larger heading and shorter measure went with the equal widths. They
     existed because cards 1 and 4 were the wide ones and editorial scaling
     follows column width; at equal widths they would just make two of the four
     arbitrarily louder than the others. */
  .cards > .card:nth-child(1) p:not(.card-kicker),
  .cards > .card:nth-child(4) p:not(.card-kicker) { max-width: 34ch; }
}

.card {
  /* The padding is a custom property because the book card's cover has to bleed
     back out through it. Writing that bleed as
     `calc(100% + clamp(28px, 3.6vw, 52px) * 2)` parses as invalid, so the whole
     declaration is dropped and the image silently falls back to content width --
     it bled off the left edge and stopped 104px short of the right. Held in a
     variable the same arithmetic is valid. */
  --card-pad: clamp(24px, 3vw, 44px);
  position: relative;
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  /* Was clamp(280px, 30vw, 400px). Cards in a row stretch to the tallest of
     the pair, and the book card is tallest because it carries a photograph, so
     every pixel of minimum height was also being added to the cyan card beside
     it -- which has the least content and so turned it all into empty colour,
     around 250px of it at 1440. Lowering the floor lets content set the height
     and the stretch only makes up the difference. */
  min-height: clamp(220px, 20vw, 300px);
}

/* The numbers are a design element in Lee's draft, not a footnote. At 13px and
   55% opacity they read as debris; at this size they anchor the top-right
   corner of each card the way the draft does. */
/* Sized off Lee's draft, where the number is an incidental marker rather than
   a second heading: roughly a quarter of the card title, not two thirds. At
   30px/700 it competed with the h2 beside it, and with the numbered section
   kicker above it the page read as two separate counting systems shouting at
   once. Both sets of numbers are his design; only the weight was ours. */
/* NO `opacity` HERE, and that is the fix rather than a tidy-up. It used to
   carry `opacity: 0.35`, which is how three of these came to be unreadable
   without anything in the stylesheet saying so: opacity paints a colour that
   appears nowhere in the source, so `color: var(--muted)` on the white card was
   really #cccccc at 1.6:1, and `color: var(--ink)` on the cyan card was
   #338387 at 1.96:1. A file that documents contrast ratios to two decimal
   places could not have caught either by reading, because neither number is
   written anywhere. Each ground now names the colour it actually paints. */
.card-num {
  position: absolute;
  top: clamp(20px, 2.6vw, 32px);
  right: clamp(22px, 2.8vw, 36px);
  font-size: clamp(13px, 1.05vw, 16px);
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1;
}

.card-kicker {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin: 0 0 auto;
  padding-right: 40px;
}

.card h2 {
  font-size: clamp(28px, 3.6vw, 44px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: var(--tight);
  margin: clamp(14px, 1.7vw, 20px) 0 12px;
}

.card p {
  margin: 0 0 18px;
  font-size: clamp(15px, 1.7vw, 17px);
  max-width: 46ch;
}

.card-cta {
  margin-top: auto;
  align-self: flex-start;
  font-weight: 700;
  font-size: clamp(15px, 1.7vw, 17px);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  padding-bottom: 2px;
  transition: border-color 0.15s ease;
}
.card-cta:hover { border-bottom-color: currentColor; }
.card-cta::after {
  content: ' \2197';
  display: inline-block;
  transition: transform 0.15s ease;
}

/* Whole-card targets. Only the cards whose call to action is a real link get
   one, which the `a.card-cta` selector is doing rather than a class: the
   podcast card has no `.card-cta` at all, because it offers four platforms
   and so has no single destination to stretch. The pseudo-element covers the
   card rather
   than the card becoming a link, so the markup keeps one anchor with real text
   in it -- a screen reader still announces "Visit Happy Days" and not the whole
   card read aloud.

   It matters most on a phone. The QR code is printed in a book, so most
   arrivals are thumbs, and a 17px line of text is a poor target where the whole
   card is a good one. */
a.card-cta::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* The two `:has()` rules below stay, and replacing them was built and rejected.
   A trace at 4x CPU throttle attributed 417ms of layout to them, reproducibly,
   over nine interleaved rounds. So they were swapped for a `.card--linked`
   class on the three cards that carry a whole-card link, with the focus ring
   moved onto the stretched anchor's own card-sized ::before -- behaviour
   verified identical, hover shadow on the same three cards and the ring still
   card-shaped.

   Lighthouse did not move: median performance 61 with `:has()` against 58
   without, largest paint 4.1s against 4.2s. No gain, and the replacement costs
   a class that has to be kept in step with the markup by hand and a focus ring
   that must be drawn inside the card, because all three set `overflow: hidden`
   and clip an outward one.

   The lesson is about the instrument rather than the selector. Trace-attributed
   "Layout" time was wrong here in the same way it was wrong when it blamed
   PostHog's cost on analytics.js: it says where time was booked, not what
   caused it. Reproducible is not the same as real -- this was reproducible and
   still measured nothing. Judge a change on the metric being optimised. */

/* No transition and no lift, only the shadow. Both of the moving parts here
   fired exactly when the pointer crossed a card, which is exactly when the
   wheel is turning: the `box-shadow` transition repainted a 28px blur across a
   668px card every frame, and the `translateY` promoted then demoted a
   compositor layer per card. Frame timings over the grid showed an 83ms frame
   against a worst of 21ms with the cursor in the margin.

   The shadow alone still answers "is this clickable", costs one paint on
   enter, and does nothing at all while scrolling. */
.card:has(a.card-cta):hover {
  box-shadow: 0 12px 28px rgba(23, 23, 23, 0.16);
}
.card:has(a.card-cta):hover .card-cta {
  border-bottom-color: currentColor;
}
.card:has(a.card-cta):hover .card-cta::after {
  transform: translate(3px, -3px);
}

/* With the link stretched across the card, a focus ring drawn around the
   anchor would outline a few words in the corner of a large block. Move it to
   the card, which is what the key press actually targets. */
.card:has(a.card-cta:focus-visible) {
  outline: 3px solid var(--cyan);
  outline-offset: 3px;
}
a.card-cta:focus-visible { outline: none; }

/* The podcast card's platform row. z-index keeps them above any stretched
   link, though that card has none, and they carry their own borders because
   four bare links in a row read as one wrapped sentence. */
.platforms {
  position: relative;
  z-index: 2;
  list-style: none;
  margin: auto 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.platforms a {
  /* inline-flex with a floor rather than inline-block: this card is the only
     one with no whole-card target, so these four chips are the real tap area
     and they were landing at 40px. The guidance floor is 44px. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  border: 1px solid rgba(255, 255, 255, 0.32);
  padding: 8px 14px;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  color: var(--white);
  transition: border-color 0.15s ease, color 0.15s ease;
}
.platforms a:hover { border-color: var(--cyan); color: var(--cyan); }

/* The speaking card's four tags. Not decoration: the card had the least
   content of the four and was being stretched to match the book card beside
   it, which carries a photograph, so the surplus became about 250px of flat
   cyan -- the most visible flaw on the page. Content closes the gap where CSS
   could only have hidden it. Dark type only; see the --cyan-card note above. */
.tags {
  list-style: none;
  margin: 0 0 18px;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.tags li {
  border: 1px solid rgba(23, 23, 23, 0.28);
  padding: 7px 13px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}

/* Per-card grounds. Each sets every colour it needs explicitly rather than
   inheriting, so a token change cannot silently leave one card's type
   unreadable against a ground that moved. */
/* The four card numbers are set to the QUIETEST colour that clears 4.5:1 on
   their own ground, not the loudest the palette can reach. They are Lee's
   design and they are an incidental marker rather than a second heading, so
   the brief is "as recessive as the standard permits" -- which is a ratio just
   over the line, held deliberately close to it. All four now land within
   0.04 of each other, so they also read as one weight across the four cards
   for the first time: they were 6.10, 3.38, 1.61 and 1.96 before, which is a
   four-times spread on four elements meant to look identical.

   Ratios are stated because they cannot be recomputed by eye, and every one is
   against the card's own background, never against --paper. */
.card--purple { background: var(--purple); color: var(--white); }
/* Full cyan, no opacity: 5.95:1 on purple, against 3.38:1 for the 0.7 this
   replaces. Same colour the black card's kicker already uses at full strength,
   so the two wide cards in row one now match. */
.card--purple .card-kicker { color: var(--cyan); }
.card--purple .card-num { color: #61c1d5; }   /* 4.53:1 on --purple */
.card--purple h2,
.card--purple .card-cta { color: var(--white); }

.card--black { background: var(--ink); color: var(--white); }
.card--black .card-kicker { color: var(--cyan); }
.card--black .card-num { color: #3f8b8e; }    /* 4.52:1 on --ink */
.card--black h2,
.card--black .card-cta { color: var(--white); }

.card--white { background: var(--white); color: var(--ink); }
.card--white .card-kicker { color: var(--purple); }
.card--white .card-num { color: #767676; }    /* 4.54:1 on white */
.card--white h2 { color: var(--ink); }
.card--white .card-cta { color: var(--purple); }

/* Dark type only. See the --cyan-card note at the top: white here is 2.26:1. */
.card--cyan { background: var(--cyan-card); color: var(--ink); }
/* --purple-dark, not --purple. The brand purple is 4.18:1 on this ground --
   the near miss that made this the least obvious of the five failures, since
   it is the same purple that is 9.43:1 on the white card beside it and reads
   as perfectly legible there. The ground moved, not the type. --purple-dark is
   already in the palette as the button hover, and it keeps the kicker purple
   where --ink would have made this the one card whose kicker is not. */
.card--cyan .card-kicker { color: var(--purple-dark); }  /* 5.65:1 on --cyan-card */
.card--cyan .card-num { color: #234648; }     /* 4.56:1 on --cyan-card */
.card--cyan h2,
.card--cyan .card-cta { color: var(--ink); }

/* The tags did not close the gap the comment above hoped they would: measured
   at 1280px the speaking card still runs 644px to match the book card's
   photograph, and 221px of that was one unbroken hole between the tags and the
   CTA. There is no more content to add -- everything on this card is already
   lifted off the book's cover -- so the surplus is split instead. Two `auto`
   margins make flexbox share the free space evenly, which turns one hole into
   two gaps that read as spacing. */
.card--cyan .tags { margin-top: auto; }
/* And gives it up as soon as the card carries a photograph. Only one element
   per card can be the pinned one: two auto margins in a flex column do not
   fight, they split the free space, which leaves the tags and the picture both
   floating with a band of flat cyan between them. The photograph is the thing
   that has to sit on the bottom edge, so the tags go back to sitting with the
   text. Same trade the CTA already makes on the other photo cards. */
.card--cyan.card--photo .tags { margin-top: 0; }

/* The book card carries the cover photograph. It sits at the bottom and is
   allowed to crop, because the mock-up is a desk shot rather than a flat
   cover and its interesting half is the book itself. */
/* The book's title is the longest on the page by a distance, and at the shared
   card size it ran to four lines and 180px in a half-width card -- against
   45px and 90px for the others. That one heading was setting the height of the
   whole second row, and because the speaking card matches it, the height it
   could not fill became that card's two holes. Sized down for this card only,
   which is the honest fix: the type is not smaller than it should be, the
   title is longer than the others. */
.card--book h2 { font-size: clamp(26px, 2.9vw, 35px); }

.card--book { padding-bottom: 0; overflow: hidden; }
.card--book .card-cta { margin-top: 0; margin-bottom: 22px; }

/* The Amazon rank, above the link. It is a `p`, so it has to sit after
   `.card p` in the file or the 17px size and 24px bottom margin win on source
   order -- the trap that has cost this stylesheet five rules. The flag is
   purple on white at 8.6:1 rather than Amazon's own orange, which would put a
   fifth colour on a four-colour page, and cyan is not available: it is a fill
   and never type or a ground for type on anything light.

   Measured at 320, 390, 1024, 1280 and 1440, the flag takes a line of its own
   and the sentence sits underneath it at every one of them: the two category
   names run to 78 characters, so the text item never has a short enough
   max-content width to share the flag's line. That is the intended reading
   -- badge, then the detail beneath -- rather than a wrap it fell into. It is
   a flex row rather than a block so the badge stays its own width instead of
   stretching, and so the pair still sets sensibly if the wording is ever
   shortened to something that does fit beside it. */
.book-rank {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
  margin: -4px 0 22px;
  max-width: 42ch;
}
.book-rank-flag {
  background: var(--purple);
  color: var(--white);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  padding: 6px 9px 5px;
  border-radius: 3px;
  white-space: nowrap;
}
.book-rank-where {
  font-size: clamp(13px, 1.35vw, 14px);
  line-height: 1.4;
  color: #4a4a4a;
}
.book-shot {
  /* `auto`, not the 8px it used to be. This card was the tallest of the four
     and so never had surplus to place. It does now: the two cards in row one
     carry photographs, `grid-auto-rows: 1fr` raises every row to match, and
     the extra height landed underneath the cover as a band of white along the
     bottom edge. Pinned, the surplus opens above the image instead, where it
     reads as spacing. */
  margin: auto calc(var(--card-pad) * -1) 0;
  width: calc(100% + var(--card-pad) * 2);
  max-width: none;
  height: var(--shot-h);
  object-fit: cover;
  /* Centred, not 42%, because the file is now pre-cropped to the band this box
     shows. The old offset was picking a band out of a full desk photograph;
     half those pixels were downloaded and thrown away by the crop. */
  object-position: 50% 50%;
}

/* The Happy Days and podcast cards carry a photograph on the same principle as
   the book card above, and the full-bleed arithmetic is the same: it only
   parses because --card-pad holds the clamp. Inline that clamp back into the
   calc() product and the whole declaration is dropped silently, leaving the
   image inset on one edge and bleeding off the other.

   `margin-top: auto` is the one difference from .book-shot, and it matters
   here. All four cards share a height set by the tallest, which is the book
   card. These two run at 35% and 44% full, so without the auto margin their
   surplus would open up *below* the photograph rather than above it, putting a
   band of flat colour along the bottom edge of the card. Pinned to the bottom,
   the picture fills a void the card already had instead of adding height to the
   grid -- which is why adding two photographs does not make the cyan card's
   documented empty space any worse. */
.card--photo { padding-bottom: 0; overflow: hidden; }
.card-shot {
  margin-top: auto;
  margin-right: calc(var(--card-pad) * -1);
  margin-bottom: 0;
  margin-left: calc(var(--card-pad) * -1);
  width: calc(100% + var(--card-pad) * 2);
  /* Required, and it undoes itself without this: the global `img` rule sets
     max-width 100%, which clamps a deliberately-widened image straight back to
     content width. */
  max-width: none;
  object-fit: cover;
}

/* One height for both, which is the whole point of the pair. The two cards are
   now the same width, so photographs of different heights at their feet would
   read as a mistake rather than as a rhyme. They were keyed separately when the
   cards were 7 and 5 columns wide. */
.card-shot { height: var(--shot-h); }

/* Off-centre on purpose. A phone stacks the cards full width, which squeezes a
   3.6:1 band into roughly 2.2:1 and crops the sides; centred, that crop lands
   between the truck's logo and Lee and loses both. */
.card--purple .card-shot { object-position: 58% 50%; }
/* The CTA already claimed `margin-top: auto` to sit at the foot of its card,
   and the photograph now claims it too. Two auto margins in one flex column do
   not fight -- flexbox splits the free space evenly between them -- but the
   result is a CTA floating in the middle of the card with a gap above and
   below it. Only one element per card can be the thing pinned to the bottom,
   and it is the photograph, so the CTA goes back to sitting with the text.

   The bottom margin is a floor under the gap above the picture, which
   `margin-top: auto` cannot provide on its own: auto distributes whatever is
   spare, and on a phone -- where the cards stack full width -- there is nothing
   spare, so the photograph came to rest hard against the link. Flex margins do
   not collapse, so both apply: the gap is never smaller than this and grows
   from here. */
.card--photo .card-cta,
.card--photo .platforms {
  margin-top: clamp(14px, 1.7vw, 18px);
  margin-bottom: clamp(18px, 2.2vw, 24px);
}

/* ---------- Contact ---------- */

.contact { background: var(--ink); color: var(--white); }
.contact .kicker { color: var(--cyan); }
.contact h2 {
  font-size: clamp(30px, 5vw, 56px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: var(--tight);
  margin: 0 0 20px;
  color: var(--white);
  max-width: 18ch;
}
/* Same `:not(.kicker)` as the About block, and the same reason: this rule was
   outranking `.kicker` on the Contact heading and rendering it at 18px. */
.contact p:not(.kicker) { max-width: 48ch; margin: 0 0 28px; font-size: clamp(16px, 1.8vw, 18px); }
/* `:not(.btn)` is load-bearing. `.contact a` is one point more specific than
   `.btn`, so without it the primary button in this section painted its label
   cyan while the identical button in the header stayed white: the same
   component in two colours on one page. */
.contact a:not(.btn) { color: var(--cyan); }

/* Two columns above 900px so the block stops being a headline stranded in a
   large black field. The primary action sits in the left column and the
   secondary links in the right, which is what gives the section a hierarchy it
   did not have when five identical pills sat in a row. */
.contact-grid { display: grid; gap: clamp(36px, 4vw, 56px); }
@media (min-width: 900px) {
  .contact-grid {
    grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    /* `stretch`, not `start`, and the divider is the reason. The right column
       holds six links against a five-field form, so started it ended about
       300px short and took its `border-left` with it -- a vertical rule that
       stopped two thirds of the way down a tall black section and read as the
       layout having come apart. Stretched, the column fills the row and the
       rule runs the full height; the links stay at the top on their own. */
    align-items: stretch;
  }
}

.contact-note {
  font-size: 15px;
  color: rgba(255, 255, 255, 0.62);
  margin: 0;
  max-width: 44ch;
}

.contact-side .card-kicker {
  color: var(--cyan);
  margin: 0 0 18px;
  padding-right: 0;
}
@media (min-width: 900px) {
  .contact-side { border-left: 1px solid rgba(255, 255, 255, 0.14); padding-left: clamp(28px, 3vw, 44px); }

  /* The column is 284px shorter than the form at 1440 and 270 at 1280, and
     stretched it spent all of that as one tail of empty black under the last
     link, with the full-height rule beside it pointing straight at it.
     `align-self: start` was tried before this and rejected for a good reason:
     the rule then stops two thirds of the way down and reads as the layout
     having come apart.

     So the surplus is split rather than removed, which is the same move the
     cyan card makes with its tags. The Happy Days note goes to the foot of the
     column, the links stay at the top, and one 284px hole becomes two gaps
     that read as spacing between two groups. The column cannot simply be made
     taller: it is six links against five form fields, and the fields cannot
     shrink, because type under 16px makes iOS Safari zoom on focus and never
     zoom back. */
  .contact-side { display: flex; flex-direction: column; }
  /* Four classes, because `.contact-side .socials + .contact-note` further
     down the file sets a 20px top margin at three classes and would otherwise
     win on source order -- the trap this stylesheet has now lost six rules to.
     Measured before and after: 284px of empty black under the column became
     212 with the flex alone, because the auto margin never applied. */
  .contact-grid .contact-side .socials + .contact-note {
    margin-top: auto;
    padding-top: clamp(24px, 3vw, 40px);
  }
}

/* Demoted from bordered pills to a plain list. LinkedIn used to look exactly
   like TikTok, which meant the page had five primary actions and therefore
   none. LinkedIn is now the button above; these are the also-rans and are
   styled to say so. */
.socials {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
}
.socials a {
  display: block;
  padding: 11px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  text-decoration: none;
  color: rgba(255, 255, 255, 0.82);
  font-weight: 500;
  font-size: 16px;
  transition: color 0.15s ease, padding-left 0.15s ease;
}
.socials a:hover { color: var(--cyan); padding-left: 6px; }
.socials a::after { content: ' \2197'; opacity: 0.5; }

/* The direct list is the fallback route, not an also-ran, so it drops the
   outbound arrow: neither a mailto nor the button-equivalent LinkedIn link is
   "elsewhere" in the sense the arrow is signalling. */
.socials--direct { margin-bottom: 32px; }
.socials--direct a::after { content: none; }
.contact-side .socials + .card-kicker { margin-top: 32px; }
.contact-side .socials + .contact-note { margin-top: 20px; }

/* Both of these exist only to out-specify `.contact a:not(.btn)`, which is
   (0,2,1) and was silently beating the (0,1,1) `.socials a` above -- so every
   social link rendered cyan and the "styled to say so" demotion in that rule
   never actually happened. The same one-point specificity trap that once
   painted the contact button's label cyan. Now the direct list is the loud one
   and Elsewhere is the quiet one, which is the hierarchy the markup implies. */
.contact .socials a { color: rgba(255, 255, 255, 0.82); }
.contact .socials--direct a { color: var(--cyan); }

/* ---------- Contact form ---------- */

/* Fields sit on --ink, so they are drawn as translucent wells rather than
   white boxes: a white input on a black section reads as a paste error and
   pulls more attention than the headline above it. */
.contact-form {
  display: grid;
  gap: 18px;
  max-width: 34rem;
  margin: 0 0 8px;
}
.form-row { display: grid; gap: 7px; }
.form-label {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.86);
}
.form-optional {
  font-weight: 400;
  color: rgba(255, 255, 255, 0.5);
  margin-left: 6px;
}
.form-input {
  /* 16px is not a taste decision. iOS Safari zooms the viewport on focus for
     anything below it, which on a phone throws the visitor into a
     horizontally-scrolled page mid-form and does not zoom back out. */
  font: inherit;
  font-size: 16px;
  min-height: 48px;
  width: 100%;
  padding: 12px 14px;
  color: var(--white);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 4px;
  /* Safari otherwise imposes its own inset shadow and corner radius, which
     leaves the inputs looking like a different component to the textarea. */
  -webkit-appearance: none;
  appearance: none;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.form-input:hover { border-color: rgba(255, 255, 255, 0.34); }
.form-input:focus {
  outline: none;
  border-color: var(--cyan);
  background: rgba(255, 255, 255, 0.1);
  /* Cyan on --ink, not on white. #5ce0e6 is a fill and never a type colour on
     a light ground, but this section's ground is ink, where it is legible and
     is already the accent on the kicker above. */
  box-shadow: 0 0 0 3px rgba(92, 224, 230, 0.25);
}
/* `:user-invalid`, not `:invalid`. Plain `:invalid` matches an empty required
   field from the moment the page loads, so every field would sit outlined red
   before anyone had touched the form. This fires only after the visitor has
   actually interacted. Browsers too old to know the selector drop this one
   declaration and still get native validation on submit. */
.form-input:user-invalid { border-color: #ff9a8a; }

.form-textarea {
  min-height: 132px;
  /* Vertical only: a horizontally resizable textarea can be dragged wider than
     its column and give the page an overflow it has nowhere else. */
  resize: vertical;
  line-height: 1.5;
}

.contact-form .btn { justify-self: start; }

/* Honeypot. Off-screen rather than `display: none`, because some bots skip
   fields they can tell are not rendered.

   The class is on the wrapping <label> now, not on the input, so the label text
   goes off-screen with the checkbox it names. The note that used to sit here
   said `aria-hidden` was unnecessary "because it has no label, so a screen
   reader announces nothing to fill in", and that had it exactly backwards:
   having no label is the defect, and it is the one form-field failure
   Lighthouse reported. An unnamed checkbox is announced as a checkbox with no
   name. See index.html for which attribute does what. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* ---------- Thanks page ---------- */

/* Dark, like the contact section it is the other half of: somebody arriving
   here has just pressed send inside a black block, and landing on cream reads
   as having been thrown out to a different site. */
.thanks {
  background: var(--ink);
  color: var(--white);
  display: flex;
  align-items: center;
  /* 180px is the header plus a typical footer, so the block fills the first
     screen without forcing a scrollbar on to a page with nothing below it.
     `svh` rather than `vh` because on mobile Safari `vh` measures the viewport
     with the address bar retracted, which overshoots by the height of the bar
     and scrolls a page that should not scroll. */
  min-height: calc(100vh - 180px);
  min-height: calc(100svh - 180px);
}
.thanks h1 {
  font-size: clamp(30px, 5vw, 56px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: var(--tight);
  margin: 0 0 20px;
  max-width: 18ch;
}
.thanks .kicker { color: var(--cyan); }
.thanks-lede {
  max-width: 48ch;
  margin: 0 0 32px;
  font-size: clamp(16px, 1.8vw, 18px);
  color: rgba(255, 255, 255, 0.86);
}
.thanks-actions { margin: 0 0 28px; }
/* Same `:not(.btn)` exclusion as the contact section, and for the same reason:
   without it the button in this block paints its label cyan while the
   identical one in the header stays white. */
.thanks a:not(.btn) { color: var(--cyan); }

/* ---------- Privacy page ---------- */

/* A reading page, so it keeps the light ground and a measure short enough to
   read rather than the 34ch the cards use for glanceable copy. */
.legal p {
  max-width: 62ch;
  margin: 0 0 14px;
  font-size: clamp(16px, 1.8vw, 17px);
  line-height: 1.6;
}
.legal h1 {
  font-size: clamp(28px, 4vw, 44px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: var(--tight);
  margin: 0 0 20px;
  max-width: 20ch;
}
.legal h2 {
  font-size: clamp(19px, 2vw, 22px);
  font-weight: 700;
  letter-spacing: var(--tight);
  margin: 34px 0 10px;
}
/* After `.legal p`, not before it: a later rule of equal specificity wins, and
   reversing these two silently loses the lede sizing. */
.legal-lede {
  font-size: clamp(17px, 2vw, 19px);
  margin-bottom: 26px;
}
.legal-updated {
  color: var(--muted);
  font-size: 15px;
  margin: 30px 0 26px;
}
/* Purple, not cyan. #5ce0e6 is 1.58:1 on this ground and is a fill only. The
   `:not(.btn)` exclusion is the same trap as the contact and thanks sections:
   this block contains a button, and without it the button paints its label
   purple while the identical one in the header stays white. */
.legal a:not(.btn) { color: var(--purple); }

/* The notice under the send button. Quiet, but immediately under the control
   that submits a stranger's name, email and phone number, which is where the
   law expects it rather than in the footer. */
.form-privacy {
  max-width: 34rem;
  margin: 4px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.62);
}

/* ---------- Footer ---------- */

.site-footer {
  background: var(--ink);
  color: rgba(255, 255, 255, 0.6);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  padding: 28px 0 40px;
  font-size: 14px;
}
.site-footer .shell {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  justify-content: space-between;
}
.site-footer a { color: rgba(255, 255, 255, 0.75); }

/* The scroll reveal is gone. It used to animate opacity and transform on all
   four cards against a `view()` timeline, which made it the only thing on the
   page doing work *while* the page scrolled -- everything else is static type
   on flat colour. Two separate reports of the scrolling feeling wrong, and the
   only candidate that runs on the scroll itself.

   It was decoration. This page exists so somebody holding a printed book can
   confirm they scanned the right code and find the podcast, and a fade-in
   contributes nothing to either. Removing it is free, and it removes the whole
   class of problem rather than tuning one instance of it.

   Kept as a note rather than deleted silently: if it ever comes back, it needs
   the `@supports (animation-timeline: view())` guard it had, because an
   unsupported browser must never be left with content stuck at opacity 0. See
   also the screenshot trap in CLAUDE.md, which that animation caused. */

/* ---------- Display type on short screens ---------- */

/* The hero already tightened on a short viewport; nothing else did, so on a
   1280x585 laptop the headline dropped to 76px while the cards behind it kept
   a 54px heading and the contact block kept 56px. That is poster type on a
   screen a quarter the height it was tuned for, and it is why the page reads
   as oversized at 100% zoom on exactly the display most desktop visitors have.

   Only the display sizes move. Body copy is 17px, the nav is 16px and the
   fields are 16px: those are already right, and shrinking them would trade a
   cosmetic complaint for an unreadable one.

   This block sits at the very end of the file on purpose. A media query adds
   no specificity, so every rule below has to come after the plain rule it
   overrides -- `.contact h2` is at ~739 and `.thanks h1` at ~923, so anywhere
   earlier and these would silently lose on source order and do nothing. */
@media (min-width: 900px) and (max-height: 720px) {
  /* 76px, not the 60px this block used to set. That 60 came from a complaint
     that the page read oversized at 100% zoom on a 1280x585 laptop, and it cut
     the hero along with the section and card headings -- which was one cut too
     many. Measured on that laptop, the 80% zoom Lee prefers puts this headline
     at 73.6 physical pixels, because 731 clears the max-height above and the
     full-size rules come back at 0.8 scale. So the size he is asking for is the
     size this had before that cut, and what he was objecting to the first time
     was the section and card headings at 56 and 44 -- which stay where that cut
     put them, because 80% zoom shows them at 44.8 and 35.2, near enough.

     Read the whole comparison in physical pixels or it inverts: in CSS pixels
     the type at 80% zoom looks larger, and the conclusion comes out backwards. */
  .hero h1 { font-size: clamp(40px, 6vw, 76px); }

  /* Moved here from the hero block at ~289, where both declarations were dead:
     `.hero-lede` is declared again at ~373 with a `margin` shorthand, later in
     the file, and a media query adds no specificity. Nothing follows this
     block, so this is the one place the override holds. */
  .hero-lede { font-size: 17px; margin-bottom: 12px; }

  .card h2 { font-size: clamp(24px, 2.7vw, 34px); }
  /* The larger heading for cards one and four was removed here as well as in
     the main block. Left behind it produced four different heading sizes across
     four cards that are now the same width -- 40, 34, 30, 40 measured at
     1280x585 -- which is the sort of thing that only shows up when the numbers
     are read off the page rather than the stylesheet. */
  .card--book h2 { font-size: clamp(22px, 2.4vw, 30px); }

  /* Every section heading, not just Contact. This block shrank Contact from
     56px and left About and What I do at 56px, so a short laptop showed two
     section titles at one size and the third at another. */
  .about h2,
  .section-title,
  .contact h2,
  .thanks h1 { font-size: clamp(26px, 3.2vw, 40px); }
}

/* The rest of the page on a short laptop, and the last piece of the 80% zoom
   report. The block above brought the display type down; measured against the
   1600x731 that 80% zoom produces, everything it did NOT touch was still
   exactly 25% larger -- body copy, buttons, the header, the nav, the form and
   the gaps. That 25% is arithmetic, not taste: 80% renders every untouched
   pixel at 0.8, and 1/0.8 is 1.25. It is the whole of what was left.

   This closes about half of it rather than all of it. Matching 80% outright
   would put card copy at 13.6px and the About passage at 14.4px, and 350 words
   of body text at 14px is trading a complaint about size for a complaint about
   reading. So the body layer comes down roughly 12%, which is most of the
   difference, and stops above the point where it costs legibility.

   Form fields are deliberately absent. They must never go below 16px or iOS
   zooms the viewport on focus and does not zoom back out. This query is
   min-width 900px and no iOS device that wide is also 720px short, so it would
   probably be safe -- "probably" is not a good enough reason to strand somebody
   halfway through typing their enquiry.

   At the end of the file for the usual reason: a media query adds no
   specificity, this stylesheet has already lost three rules to source order,
   and several of the selectors below are declared again further up. */
@media (min-width: 900px) and (max-height: 720px) {
  .card p { font-size: 15px; }
  .card-kicker { font-size: 15px; }
  /* Both exclusions are load-bearing, and this one is specificity rather than
     source order. A plain `.about-copy p` is 0,1,1 and loses outright to the
     `.about-copy p:not(.kicker)` above at 0,2,1, whatever the order. Matching
     that and adding :not(.about-pull) takes it to 0,3,1, which beats the
     paragraph rule without also flattening the pull quote to body size. */
  .about-copy p:not(.kicker):not(.about-pull) { font-size: 16px; }
  .eyebrow { font-size: 13px; }

  .platforms a { font-size: 13px; }
  .form-label { font-size: 13px; }

  /* .socials sits inside .contact, where `.contact a:not(.btn)` outranks a
     plain `.socials a`. Matching that specificity rather than fighting it. */
  .contact .socials a,
  .socials a { font-size: 15px; }

  .btn { font-size: 15px; }

  /* Not a bare `.site-header a`: that is 0,1,1 and would beat the 0,1,0 of
     `.wordmark`, shrinking Lee's name to nav size. */
  .site-header a:not(.wordmark) { font-size: 15px; }
  .site-header .wordmark { font-size: 21px; }

  .site-header .shell { min-height: 72px; }
  .cards { gap: 16px; }
}

/* The tablet measure.

   Between 480px and 900px the hero is one column, and the column is wider than
   anything in it wants to be. Measured at 768 before this rule, the four
   elements finished at four different places: headline 728, book flag 582,
   lede 533, portrait 470. At 899 the spread was 127px and the portrait left
   429px of empty ground beside it, more than half the column. It read as a
   phone layout stretched sideways, because that is what it was.

   One measure fixes it. The lede, the flag and the portrait now end together
   at 600, and the headline still spans the full column above them -- one wide
   element over one aligned block, rather than four ragged edges. Everything
   stays flush left at the gutter; only the right edge changed.

   THE PORTRAIT CHANGES SHAPE HERE, and that is the part that is not obvious.
   Widening a square from 430 to 560 also makes it 130px taller, which took the
   hero from 977px to 1107px and pushed the photograph off a 1024-tall iPad
   entirely -- the aligned edges would have been bought with a hero nobody could
   see the foot of. At 3/2 the same 560px width is 373px tall, so the hero comes
   out at 920px, SHORTER than the 977 it started at, and the whole photograph
   fits the screen. Aligned and shorter, rather than aligned or shorter.

   `object-position` is 28% rather than centred because the crop takes its 187px
   out of the top and bottom of a head-and-shoulders frame. Centred, it trimmed
   the top of his head; biased up, it keeps the headroom and takes the loss off
   the chest, which has nothing in it. Judge any change to this by rendering it
   at 768 and 899, never by the ratio.

   4/3 and 16/9 were both built and measured. 4/3 (560x420) aligns and saves
   only 10px of hero height, so it keeps the photograph overrunning the fold.
   16/9 (560x315) is shorter still but crops to a letterbox that reads as a
   banner rather than as a portrait of the person.

   At the end of the file for the reason the block above gives: `.hero-lede`
   carries its own `max-width` at ~499 and `.portrait` its own at ~473, both
   later in source than the tablet query that used to hold this. */
@media (min-width: 480px) and (max-width: 899px) {
  .hero-lede,
  .hero .book-flag { max-width: 560px; }

  .portrait {
    max-width: 560px;
    aspect-ratio: 3 / 2;
    object-position: 50% 28%;
  }
}


