/* ============================================================
   THE DISH — base stylesheet
   Imports tokens.css for every value. Declares no raw colour or
   type. Comp B layers site-b.css on top of this.
   ============================================================ */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);
  font-size:var(--step-0);line-height:1.6;-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
/* Page measure. 52rem was too narrow: with a 15rem portrait beside it the
   hero text column came out at 480px on a 1200px screen, which is why the
   headline needed shrinking to break cleanly. 68rem gives the page room
   and the text column 624px. Prose measure is protected separately, by
   max-width on the paragraphs, so widening the page does not widen the
   reading line past comfort. */
:root{--wrap:68rem;
  /* Fluid, not fixed. At a flat 22rem the portrait squeezed the headline
     column at exactly 900px wide, just past the stacking breakpoint, and
     the hero broke to "I write about / baseball / for The Athletic." with
     one word stranded. The clamp gives the portrait back some width in
     that band and the fault disappears at every size tested. */
  --aside:clamp(15rem, 26vw, 22rem);
  --stack:56rem}
.wrap{max-width:var(--wrap);margin:0 auto;padding-inline:clamp(1.25rem,5vw,2rem)}
.skip{position:absolute;left:-9999px}
.skip:focus{left:1rem;top:1rem;z-index:9;background:var(--ink);color:var(--paper);padding:.6rem 1rem}

/* --- nav: five items, no rules, no colour --- */
.top{padding-block:1.5rem}
.top .wrap{display:flex;align-items:center;gap:1.25rem;flex-wrap:wrap}
/* Twice the size it was, mark and name together. At 38px and 17px the
   lockup read as a favicon that had wandered into the header. */
.brand{display:flex;align-items:center;gap:.85rem;font-family:var(--display);font-weight:700;
  font-size:2.125rem;letter-spacing:-0.02em;margin-right:auto;white-space:nowrap;line-height:1}
.top nav{display:flex;gap:clamp(.85rem,2.5vw,1.5rem);flex-wrap:wrap}
.top nav a{font-size:.9375rem;color:var(--ink-2)}
.top nav a:hover{color:var(--accent)}

/* --- the mark ---------------------------------------------------
   The red spine is already there when the page paints. Only the two
   angled strokes draw: the left page falls in, and the right page
   leaves at the moment the left one lands. It runs once and stops. A
   header logo that redraws every six seconds reads as a bug.

   Drawing the spine as well made the mark arrive as three separate
   events. Holding the spine still gives the two pages something to
   land on, which is what the bounce is.

   stroke-dasharray is 100 200, longer than the path, and the rest
   offset is 101 rather than 100. At exactly 100 a dash boundary sits
   on path distance zero, and a zero-length dash with a round cap
   paints a filled circle. That was the dot that appeared before each
   stroke drew. At 101 the preceding dash ends off the path, so there
   is no cap to paint and nothing renders until the offset moves.

   stroke-width is in user units, so it scales with the SVG. Do not
   reach for vector-effect:non-scaling-stroke here; it pins the stroke
   to CSS pixels and blobs the mark at favicon size.
   ---------------------------------------------------------------- */
.dish-mark{display:inline-block;line-height:0;
  --mark-w:76px; --sw:9; --sw-draw:6.2;
  --in:1.05s;   /* the fall. short, because it is fast */
  --out:1.7s}   /* the rise. longer, because it has lost energy */
.dish-mark svg{display:block;overflow:visible;width:var(--mark-w);height:auto}
.dish-mark path{fill:none;stroke:var(--ink);
  stroke-linecap:round;stroke-linejoin:round;stroke-width:var(--sw);
  stroke-dasharray:100 200;stroke-dashoffset:101}
/* the spine: present from the first paint, never animated */
.dish-mark .accent{stroke:var(--accent);animation:none;
  stroke-dashoffset:0;stroke-width:var(--sw)}
@keyframes dish-draw{from{stroke-dashoffset:101}to{stroke-dashoffset:0}}
@keyframes dish-press{from{stroke-width:var(--sw-draw)}to{stroke-width:var(--sw)}}

/* THE BOUNCE.
   One easing curve for both strokes made this a drawing, not a bounce. A
   ball does two different things either side of the contact point and the
   mark has to do them too.

   Falling: it accelerates. Slowest at the top, fastest at the moment it
   hits. That is ease-IN, and the path is drawn from the top-left corner
   down to the contact point, so the acceleration runs the right way.

   Rising: it leaves with less energy than it arrived with and decelerates
   the whole way up. That is ease-OUT, over a longer duration. The second
   stroke is drawn from the contact point outward, so again the direction
   matches.

   The durations carry as much of it as the curves do. Equal durations read
   as a machine tracing a shape. 1.05s down against 1.7s up is the energy
   the bounce lost. */
.dish-mark path:nth-child(2){          /* the fall, accelerating into contact */
  animation:dish-draw var(--in) both, dish-press var(--in) both;
  animation-delay:0s,0s;
  animation-timing-function:cubic-bezier(.42,0,1,1)}   /* fallback: ease-in */
.dish-mark path:nth-child(3){          /* the rise, decelerating away */
  animation:dish-draw var(--out) both, dish-press var(--out) both;
  animation-delay:var(--in),var(--in);
  animation-timing-function:cubic-bezier(0,0,.58,1)}   /* fallback: ease-out */

/* The curves are not eyeballed. They are the motion itself, sampled at eight
   points and handed to linear().

   Falling from rest under gravity, distance is proportional to time squared,
   so progress is t^2:            0, .0156, .0625, .1406, .25, .3906, .5625, .7656, 1
   Rising and decelerating to a stop is the same parabola reversed,
   so progress is 2t - t^2:       0, .2344, .4375, .6094, .75, .8594, .9375, .9844, 1

   The first attempt guessed a pair of beziers and the rise was 87% drawn in
   its first quarter, which reads as a whip rather than a ball. The parabola
   puts it at 44%, and the check enforces that. */
@supports (animation-timing-function:linear(0,1)){
  .dish-mark path:nth-child(2){animation-timing-function:linear(
    0,.0156,.0625,.1406,.25,.3906,.5625,.7656,1)}
  .dish-mark path:nth-child(3){animation-timing-function:linear(
    0,.2344,.4375,.6094,.75,.8594,.9375,.9844,1)}}
@media (prefers-reduced-motion:reduce){
  .dish-mark path{animation:none;stroke-dashoffset:0;stroke-width:var(--sw)}}

/* --- line breaking ------------------------------------------------
   Headlines get balance so the last line is never one orphaned word,
   and body copy gets pretty for the same reason a paragraph at a time.
   Proper nouns are held together with hard spaces in build.py, which
   is the only fix that works: no CSS property knows that The Athletic
   is one name.
   ---------------------------------------------------------------- */
/* pretty, not balance, on the headlines. Balance evens the line lengths
   and on the hero it did that by stranding "Everything" alone on a line,
   which is the exact fault it is meant to prevent. pretty keeps whole
   phrases together at every width tested. Balance stays on the short
   standfirsts, where evening two lines is the right instinct. */
h1,h2,h3{text-wrap:pretty}
.page-head p,.hero p.sub{text-wrap:balance}
p,li{text-wrap:pretty}

/* --- hero: line, form, portrait --- */
.hero{padding-block:clamp(2rem,6vw,4.5rem) clamp(2rem,5vw,3.5rem)}
.hero .grid{display:grid;grid-template-columns:minmax(0,1fr) var(--aside);gap:clamp(1.75rem,5vw,3rem);align-items:start}
@media (max-width:56rem){.hero .grid{grid-template-columns:1fr}}
/* Back to full step 3. It was capped at 2.7rem while the page measure was
   52rem, because a 480px column could not hold "for The Athletic." beside
   anything else. At 68rem with a fluid aside the column is wide enough and
   the rag stays clean at every width. build/rag3.mjs sweeps headline size
   against aside width and reports any line left holding a single word. */
.hero h1{font-family:var(--display);font-weight:800;font-size:var(--step-3);
  line-height:1.1;letter-spacing:-0.022em;margin:0 0 .9rem}
.hero p.sub{color:var(--ink-2);font-size:var(--step-1);margin:0 0 1.75rem;max-width:32rem}

/* One email field does not want a 900px box. The cap holds the form at a
   size that reads as a form rather than as a section. */
.signup{border:1px solid var(--line-2);padding:clamp(1rem,3vw,1.4rem);
  background:var(--paper-2);max-width:36rem}
.signup h2{font-size:1rem;font-weight:600;margin:0 0 .35rem} /* 600 not 650: Instrument Sans is static */
.signup p{margin:0 0 .9rem;color:var(--ink-2);font-size:.9375rem}
.field{display:flex;flex-wrap:wrap;gap:.5rem}
.field input{flex:1 1 12rem;min-width:0;border:1px solid var(--line-2);background:var(--paper);
  color:var(--ink);font:inherit;font-size:.9375rem;padding:.7rem .8rem}
.field input::placeholder{color:var(--ink-3)}
.field button{border:0;background:var(--accent);color:var(--on-accent);cursor:pointer;
  font:inherit;font-weight:600;font-size:.9375rem;padding:.7rem 1.3rem}
.field button:hover{background:var(--accent-deep)}

.portrait{width:100%;aspect-ratio:4/5;border:1px solid var(--line-2);background:var(--paper-3);
  display:grid;place-items:center;text-align:center;padding:1rem}
/* Landscape on a phone. This block MUST come after the rule above: a media
   query adds no specificity, so when it sat earlier in the file the 4:5 won at
   every width and the mobile rule did nothing at all. */
@media (max-width:56rem){
  .portrait{aspect-ratio:16/9}
}
.portrait span{font-family:var(--mono);font-size:.5625rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);line-height:1.7}

/* The real hero portrait: a box he stands out of.
   Two classes deep so it beats both the .portrait base rule above and the 16:9
   mobile rule below it, wherever any of them sit in the file.

   This was an arch until 6 Sep. The arch read as a tombstone, which is not a
   thing you want next to a living author's name. Same idea, square corners.

   Four things are load-bearing here and none of them are decoration:

   1. The box starts at 20% down, so his head comes out over the top of it. The
      frame is therefore something he is in front of rather than inside, and the
      cut at the bottom belongs to the box rather than to the photograph.
   2. The box has no fill. The paper and the hero's ledger run through it
      untouched, so he stands on the page rather than on a tile laid over it.
      A translucent wash was tried first and dropped: even at 8% it read as a
      grey box, and the drawn rectangle alone does the job.
   3. His right shoulder runs into the box's side. That shoulder is clipped in
      the source file, and the frame edge is what makes the clip read as a crop
      rather than as a mistake. The left shoulder runs off inside the frame,
      which Logan approved on 5 Sep. Do not "fix" it.
   4. overflow:hidden on the wrapper is what cuts the shirt at the foot of the
      box, so it cannot be removed. */
.portrait.portrait--photo{position:relative;border:0;padding:0;background:none;
  aspect-ratio:4/5;display:block;overflow:hidden}
/* The box itself, behind him: a drawn rectangle and nothing else. No fill, so
   the paper and the ledger run through it untouched. A hairline in ink, not in
   --line-2: at the pale value the bottom edge was too polite to make the cut
   look deliberate, which was the entire job of the frame. */
.portrait.portrait--photo::before{content:"";position:absolute;
  left:0;right:0;top:20%;bottom:0;
  background:none;border:1px solid var(--line-ink)}
.portrait.portrait--photo picture{display:block}
.portrait.portrait--photo img{position:absolute;left:0;bottom:0;
  width:100%;height:auto;display:block}
@media (max-width:56rem){
  /* Below the grid breakpoint the aside becomes a full-width row, and a head
     as wide as the page is absurd. Cap it and centre it. */
  .portrait.portrait--photo{max-width:19rem;margin-inline:auto}
}

/* Real jackets. The placeholder .cov is a dashed box that announces itself as
   missing; a real cover needs the opposite, a hairline and nothing else. */
.cov--real{border:1px solid var(--line-2);background:none;padding:0;
  aspect-ratio:2/3;display:block}
.cov--real img{width:100%;height:auto;display:block}

/* --- sections --- */
section{padding-block:clamp(1.75rem,4vw,2.75rem);border-top:1px solid var(--line)}
h2.sec{font-family:var(--display);font-weight:800;font-size:var(--step-2);
  letter-spacing:-0.02em;margin:0 0 1.25rem}

.books{display:grid;gap:1.75rem}
.book{display:grid;grid-template-columns:5.5rem minmax(0,1fr);gap:0 1.25rem;align-items:start}
.book>*:not(.cov){grid-column:2}
.book .cov{grid-column:1;grid-row:1 / span 4;width:5.5rem}
@media (max-width:30rem){
  .book{grid-template-columns:1fr}
  .book>*:not(.cov),.book .cov{grid-column:1}
  .book .cov{grid-row:auto;margin-bottom:.6rem}
}
.book h3{font-family:var(--display);font-weight:800;font-size:var(--step-1);
  letter-spacing:-0.015em;margin:0;line-height:1.25}
.book .meta{font-size:.875rem;color:var(--ink-3)}
.book p{margin:.3rem 0 .5rem;color:var(--ink-2);max-width:38rem}
.book .buy{display:flex;gap:1rem;flex-wrap:wrap;font-size:.9375rem}
.book .buy a{color:var(--accent);border-bottom:1px solid currentColor;padding-bottom:1px}
.book--next h3{color:var(--ink-3)}

.rows{list-style:none;margin:0;padding:0;display:grid;gap:.9rem}
.rows li{display:grid;grid-template-columns:6rem minmax(0,1fr);gap:1rem;align-items:baseline}
@media (max-width:34rem){.rows li{grid-template-columns:1fr;gap:.15rem}}
.rows .d{font-family:var(--mono);font-size:.75rem;color:var(--ink-3)}
.rows a{font-size:var(--step-1);font-family:var(--display);font-weight:650;letter-spacing:-0.01em}
.rows a:hover{color:var(--accent)}
.more{display:inline-block;margin-top:1.1rem;color:var(--accent);
  border-bottom:1px solid currentColor;padding-bottom:1px;font-size:.9375rem}

.about p{color:var(--ink-2);margin:0 0 1rem;max-width:38rem}

footer{border-top:1px solid var(--line);padding-block:2rem 3rem;margin-top:1rem}
footer .row{display:flex;gap:1.1rem;flex-wrap:wrap;margin-bottom:1.1rem}
footer a{font-size:.9375rem;color:var(--ink-2)}
footer a:hover{color:var(--accent)}
footer .legal{font-size:.8125rem;color:var(--ink-3)}

/* Stats band. The em dash count was always meant to sit beside the post count
   and the book count, not stand alone: three numbers, one of which is a joke,
   and the joke only works because the other two are dry.
   It goes AFTER the signup so it cannot push the form down the page.
   BOTH NUMBERS ARE PLACEHOLDERS. build/count-em-dashes.mjs returns the real
   em dash total and the real post count in the same pass: the WordPress REST
   API sends the post count back in the x-wp-total header. One run, two numbers. */
.stats{border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  background:var(--paper-2)}
.stats .wrap{display:flex;gap:clamp(1.75rem,6vw,4rem);flex-wrap:wrap;
  padding-block:clamp(1rem,3vw,1.5rem)}
.stat .n{display:block;font-family:var(--display);font-weight:800;
  font-size:var(--step-2);line-height:1;letter-spacing:-0.02em;color:var(--accent)}
.stat .l{display:block;font-family:var(--mono);font-size:.5625rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-3);margin-top:.45rem}
.stat--joke .n{color:var(--ink)}
.stat--joke .f{display:block;font-family:var(--quote);font-style:italic;
  font-size:.875rem;color:var(--ink-3);margin-top:.2rem}

/* Jacket slot. Fixed at 2:3, which is a 6x9 trade hardcover, so the real
   image drops in with no reflow. The label is a slot marker, not a design
   element: a box styled to look like a cover reads as a decision rather than
   as something still missing. */
.cov{aspect-ratio:2/3;background:var(--paper-3);border:1px dashed var(--line-2);
  display:grid;place-items:center;text-align:center;padding:.5rem;overflow:hidden}
.cov img{width:100%;height:100%;object-fit:cover;display:block}
.cov span{font-family:var(--mono);font-size:.5rem;letter-spacing:.12em;
  /* explicit weight. In comp B this span sat inside a rule that set 800 for the
     old fake cover, and IBM Plex Mono has no 800, so it was being faked. */
  font-weight:500;
  text-transform:uppercase;color:var(--ink-3);line-height:1.8}

/* --- page furniture shared by the inner pages --- */
.page-head{padding-block:clamp(1.75rem,5vw,3rem) clamp(1rem,3vw,1.5rem)}
.page-head h1{font-family:var(--display);font-weight:800;font-size:var(--step-3);
  line-height:1.1;letter-spacing:-0.022em;margin:0 0 .6rem}
.page-head p{color:var(--ink-2);font-size:var(--step-1);margin:0;max-width:34rem}
.prose p{color:var(--ink-2);margin:0 0 1.05rem;max-width:38rem}
.prose h3{font-family:var(--display);font-weight:800;font-size:var(--step-1);
  letter-spacing:-0.015em;margin:1.75rem 0 .5rem}
.two-col{display:grid;grid-template-columns:minmax(0,1fr) var(--aside);
  gap:clamp(1.75rem,5vw,3rem);align-items:start}
@media (max-width:56rem){.two-col{grid-template-columns:1fr}}
.subjects{list-style:none;margin:1rem 0 0;padding:0;display:grid;gap:.55rem}
/* The category labels were 9px, which is a footnote size doing the job of a
   heading. These name the five subjects the whole site is about, so they get
   read as labels rather than squinted at. Larger caps also need less tracking,
   hence .1em rather than .14em, and a wider column so BOARD GAMING holds one
   line. */
.subjects li{display:grid;grid-template-columns:9.5rem minmax(0,1fr);gap:1rem;
  align-items:baseline;padding-block:.65rem;border-top:1px solid var(--line)}
.subjects .k{font-family:var(--mono);font-size:.8125rem;font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}
.subjects .v{color:var(--ink-2)}
@media (max-width:34rem){.subjects li{grid-template-columns:1fr;gap:.15rem}}
.reach{list-style:none;margin:0;padding:0;display:grid;gap:1.25rem;max-width:34rem}
.reach li{border-top:1px solid var(--line);padding-top:.9rem}
.reach .k{display:block;font-family:var(--mono);font-size:.5625rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--accent);margin-bottom:.3rem}
.reach p{margin:.2rem 0 0;color:var(--ink-2);font-size:.9375rem}
.reach a{color:var(--accent);border-bottom:1px solid currentColor;padding-bottom:1px}

/* About page photo pair. Two 4:5 portraits stacked on a phone is 700px of grey
   before the reader reaches anything. The casual one runs landscape. */
.photos{display:grid;gap:.9rem}
.photos .portrait--casual{aspect-ratio:16/9}
