/* =========================================================
   Monochrome Field Notes — shared stylesheet
   Ported from the design canvas mockups (personal-website-plan.md
   §10.5, canvas artboards MonochromeFieldNotes/About/ProjectPage/
   WritingPost/PhotoAlbum/ReadingList/PhotoLightbox). One stylesheet
   for every public template rather than per-page styles, since the
   whole site shares one type scale, one palette, and one set of
   repeating components (bracketed section labels, hairline rules,
   the org/role timeline, the photo box).

   Adaptation from the mockups: the mockups had no real photos, so
   every "photo" was a gradient box with a halftone dot overlay for
   texture. That overlay stays for genuine placeholders (.photo.pending
   — nothing uploaded yet) but is dropped for boxes holding a real
   <img>, where it would just degrade the photo.
   ========================================================= */

:root {
  --bg: #f4f3ef;
  --ink: #1c1c1a;
  --ink-soft: #33322e;
  --prose: #2b2a27;
  --muted: #8a8880;
  --muted-2: #b0ada2;
  --rule: #d6d4cc;
  --rule-dashed: #c9c2a8;
  --accent: #3a6ea5;

  /* The one text-column width shared by every "plain content page" --
     About, Contact, Reading, Writing index, Writing post (no TOC),
     Projects index, Project detail. Before this, those pages used four
     different values (860/780/700/620px) picked independently per
     mockup artboard, which left/right-aligns each page's content at a
     different distance from center -- the "left edge jumps around"
     feedback.

     Originally the home hero and photography grids were kept off this
     variable entirely (full-bleed/grid layouts, not text columns). Two
     of those -- .hero and .album-grid (the all-albums index) -- were
     brought onto it after feedback that Home and the album listing
     should read as the same width as every other page, not wider. The
     one holdout is .mosaic (an individual album's photo grid): it's
     deliberately matched to .album-header's width instead (see the
     comment above .mosaic) rather than to this variable, since on that
     one page "the rest of the content" is the header, not the site-wide
     standard. */
  --content-width: 860px;

  /* The outer page wrapper -- same box .site-main below is capped at.
     Round 27 added this as a named variable (was a bare 1320px on
     .site-main alone) so content-body images/video can size
     themselves as "percent of the site wrapper" without duplicating
     the number -- see the image-figure/video-figure comment further
     down. */
  --wrapper-width: 1320px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  height: 100%;
}

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Nav and footer are full-bleed (direct children of <body>, no wrapper) --
   they span the whole viewport width on purpose. Only the content column
   is capped and centered, at the same width the design canvas artboards
   were drawn at (1280px) -- without this, a wide viewport just
   left-aligns the 80px-padded sections against the browser edge instead
   of centering them like the mockups.

   .site-main also carries the sticky-footer job: `flex: 1 0 auto` on a
   flex-column body is what makes it grow to fill leftover height on a
   short page, so the footer lands at the true bottom of the viewport
   instead of floating up against short content. */
.site-main {
  width: 100%;
  max-width: var(--wrapper-width);
  margin: 0 auto;
  flex: 1 0 auto;
}

a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
a:hover { color: var(--accent); }

img { max-width: 100%; display: block; }

/* ---------- nav / footer (every page) ---------- */
.nav {
  display: flex; justify-content: space-between; align-items: center;
  padding: 34px 80px; border-bottom: 2px solid var(--ink);
  flex-wrap: wrap; gap: 12px;
}
.wordmark { font-weight: 600; font-size: 16px; text-transform: uppercase; letter-spacing: 0.05em; }
.wordmark a { text-decoration: none; color: inherit; }

/* Checkbox-hack menu toggle for the phone nav (see the mobile section
   at the end of this file for the part that actually shows/hides
   anything) -- a plain checkbox + its <label>, no JS. Hidden here
   unconditionally: on every width above the phone breakpoint .navlinks
   is just shown inline as it always was, so the toggle has nothing to
   do and stays out of the layout entirely. */
.nav-toggle-input { position: absolute; opacity: 0; pointer-events: none; }
.nav-toggle { display: none; }

.navlinks { display: flex; gap: 4px; font-size: 13px; flex-wrap: wrap; }
.navlinks a {
  text-decoration: none;
  padding: 4px 10px 12px;
  background-repeat: repeat-x;
  background-position: bottom 4px left 10px;
  background-size: 24px 6px;
}
/* Same tiling approach as before (a seamless Q...T quadratic pair -- see
   the git history for why plain repeat needs matched start/end tangents),
   but re-tuned to read as gentle and low, like the headline's hand-drawn
   curve, instead of a tight, tall zigzag. The earlier version packed a
   full up-down cycle into 20x8 with a 4-unit swing off the baseline --
   proportionally a much steeper wave than the headline's (the headline
   spans ~300 units of width for a swing of ~4-5, i.e. a long, shallow
   curve). This tile keeps the same idea at nav scale rather than
   literally reusing the headline's numbers (that curve tiled onto a
   ~50px-wide nav word would barely complete once) -- same stroke
   thickness (1.3), shorter box (6 instead of 8), and about half the old
   vertical swing (2 units instead of 4), so each link still gets a full
   wavy line, just a flatter, shorter one. */
.navlinks a:hover,
.navlinks a.current {
  color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 6' width='24' height='6'%3E%3Cpath d='M0,3 Q6,1 12,3 T24,3' fill='none' stroke='%233a6ea5' stroke-width='1.3'/%3E%3C/svg%3E");
}
.navlinks span { color: var(--muted-2); padding: 4px 0; }

.footer {
  display: flex; justify-content: space-between; padding: 30px 80px 48px;
  border-top: 2px solid var(--ink); font-size: 12px; color: var(--muted);
  flex-wrap: wrap; gap: 12px;
}
.footer-links { display: flex; gap: 16px; }
.footer-links a:hover { color: var(--accent); }

/* ---------- bracketed section labels + hairlines ---------- */
section.block { padding: 0 80px 68px; }
section.block.narrow { max-width: var(--content-width); margin: 0 auto; padding: 0 0 60px; }
section.block h2 {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em;
  font-weight: 500; color: var(--ink); margin: 0 0 22px;
}
section.block h2::before { content: "["; color: var(--accent); margin-right: 6px; }
section.block h2::after { content: "]"; color: var(--accent); margin-left: 6px; }

.kicker { font-size: 12px; color: var(--muted); margin: 0 0 18px; }
.kicker.slash::before { content: "// "; }

.squiggle { position: relative; display: inline-block; }
.squiggle svg { position: absolute; left: 0; bottom: -8px; width: 100%; height: 10px; overflow: visible; }

/* ---------- hero (home) ---------- */
/* Bottom padding is deliberately smaller than a normal section's (68px,
   see section.block) because the hero row's real height is set by
   whichever column is taller -- almost always the photo (480px + a
   caption), not the shorter intro-text column next to it. That leftover
   height already reads as a gap above "Recent Writing" on its own, so
   stacking a full 60-68px of padding on top of it made the gap under a
   tall photo look much bigger than the same gap anywhere else on the
   page. 32px here plus the photo's own trailing space lands close to
   the other sections' 68px, rather than well past it. */
.hero { display: flex; gap: 56px; align-items: flex-start; padding: 84px 0 32px; flex-wrap: wrap; max-width: var(--content-width); margin: 0 auto; }
.hero-text { flex: 1; min-width: 320px; }
.hero h1 { font-size: 40px; margin: 0 0 22px; font-weight: 500; text-wrap: pretty; }
.hero .intro { font-size: 15px; line-height: 1.8; color: var(--ink-soft); max-width: 520px; margin: 0 0 22px; }
.now-line { font-size: 12.5px; line-height: 1.7; color: #57554d; border-left: 2px solid var(--accent); padding-left: 14px; max-width: 500px; }
.now-line b { color: var(--ink); font-weight: 600; }
/* margin:0 undoes the browser default UA stylesheet's `figure { margin:
   1em 40px }` -- never reset here before, and harmless while .hero
   tracked the old full ~1160px width (plenty of slack to absorb 80px of
   unaccounted-for side margin), but at the new 860px --content-width
   that 80px was enough to bust the row's width budget and silently
   force .hero-photo to wrap onto its own line below .hero-text, even
   though .hero-text's own min-width (320px) left more than enough room
   on paper. Reported as "put the hero photo back on the right like it
   was" -- confirmed via computed-style inspection (margin-left/right:
   40px each, un-set by any rule here) before touching the width, so
   this isn't a guess. Width also trimmed per request ("a little
   smaller"), 420px -> 360px, which independently would have masked the
   margin bug too -- fixed both since relying on enough slack to hide an
   unreset default margin is exactly the kind of thing that breaks again
   next time the width changes. */
/* 320x400 (4:5) is a standard portrait-photo ratio -- was 360x360
   (square), which forced object-fit:cover to crop the top/bottom off
   any portrait-oriented source photo, making it look squat. */
.hero-photo { flex-shrink: 0; width: 320px; margin: 0; }
.hero-photo .photo { height: 400px; }
.hero-photo figcaption { margin-top: 10px; font-size: 11.5px; color: var(--muted); }

/* ---------- generic page header (About/Projects/Writing/Reading) ---------- */
.header { padding: 76px 0 44px; max-width: var(--content-width); margin: 0 auto; }
.header .intro, .header > p { font-size: 14px; line-height: 1.75; color: #57554d; max-width: 620px; }
.header h1 { font-size: 36px; margin: 0 0 16px; font-weight: 500; }

/* ---------- writing list ---------- */
.writing-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
.writing-list li { display: flex; align-items: baseline; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--rule); flex-wrap: wrap; }
.writing-list .tag { font-size: 11px; color: var(--accent); flex-shrink: 0; width: 90px; }
.writing-list a { font-size: 15px; flex-grow: 1; }
.writing-list .date { font-size: 12px; color: var(--muted); flex-shrink: 0; }

.tag-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 30px; }
.tag-filter a { font-size: 11px; padding: 4px 10px; border: 1px solid var(--rule); text-decoration: none; color: var(--muted); }
.tag-filter a:hover { border-color: var(--accent); color: var(--accent); }
.tag-filter a.current { border-color: var(--accent); color: var(--accent); background: rgba(58,110,165,0.08); }

/* ---------- project grid / cards ---------- */
.project-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
@media (max-width: 900px) { .project-grid { grid-template-columns: 1fr; } }
.card { background: var(--bg); padding: 24px; }
.card h3 { font-size: 14px; margin: 0 0 12px; font-weight: 600; }
.card h3::before { content: ""; display: inline-block; width: 7px; height: 7px; background: var(--accent); margin-right: 9px; vertical-align: middle; }
.card p { font-size: 13px; line-height: 1.65; color: #4a4a45; margin: 0; }
.card .meta { font-size: 11px; color: var(--muted); margin-top: 10px; }

/* ---------- photo boxes: real image or placeholder ---------- */
.photo { position: relative; overflow: hidden; background: var(--ink); }
.photo img { width: 100%; height: 100%; object-fit: cover; }
.photo.pending {
  display: flex; align-items: center; justify-content: center; text-align: center;
  border: 1px dashed var(--rule-dashed); background: transparent;
}
.photo.pending span { color: var(--muted); font-style: italic; font-size: 11px; padding: 8px; }
.photo .cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  font-size: 11px; color: var(--bg); padding: 10px 14px;
  background: linear-gradient(to top, rgba(0,0,0,0.55), transparent);
}

/* Round 14 brought this from var(--ink) (near-black) to var(--rule)
   (the same light hairline .album-grid uses for its own tile seams) --
   fine as "not a black bar," but round 17 feedback asked for the seams
   to match the page background exactly, not just be lighter than
   before. var(--bg) is body's own background (see the html/body rule
   near the top of this file), so the 3px gaps now read as no seam at
   all rather than a hairline. */
.gallery { display: grid; grid-template-columns: 1.4fr 1fr; grid-template-rows: repeat(2, 1fr); gap: 3px; background: var(--bg); height: 420px; }
.gallery .photo.large { grid-row: 1 / span 2; }
/* Round 22: each gallery tile links to its album via <a class="photo">
   directly -- the anchor itself IS the grid item / photo box, not a
   wrapper around one. Round 13 used a <div class="photo"> for the box
   and wrapped it in an <a> set to `display: contents`, on the
   reasoning that the div would still work as the grid item and the
   anchor would still catch the click since its content stays part of
   its hit area. That held up in the browsers checked at the time, but
   Safari has a long-standing bug where a `display: contents` anchor
   stops responding to clicks/taps entirely -- the image and caption
   render exactly as before, nothing happens when you touch or click
   them. Since this teaser only ever shows albums that already have a
   cover photo (get_home_gallery filters out any that don't), there's
   no "pending" placeholder state to preserve here, so the anchor can
   just BE the .photo box directly -- same classes, same look, and a
   real clickable element in every browser. */
.gallery a.photo { display: block; color: inherit; text-decoration: none; }

/* ---------- footer nav wordmark link on every page uses .wordmark a ---------- */

/* ---------- About: org/role timeline ---------- */
.org { margin-bottom: 50px; }
.org:last-child { margin-bottom: 0; }
.org-head { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; }
.org-icon { width: 34px; height: 34px; flex-shrink: 0; border: 1.5px solid var(--ink); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; letter-spacing: 0.02em; overflow: hidden; }
.org-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
.org-name { font-size: 15px; font-weight: 600; margin: 0; }
.org-sub { font-size: 12px; color: var(--muted); margin: 2px 0 0; }

.roles { border-left: 2px solid var(--rule); margin-left: 16px; padding-left: 28px; }
.role { position: relative; padding-bottom: 34px; }
.role:last-child { padding-bottom: 0; }
/* Positioned by computing WHERE the rule line is (.roles' own
   padding-left + border-left-width, i.e. -30px back from .role's own
   left edge) and then centering the dot ON that point with
   `transform: translateX(-50%)`, instead of the previous approach of
   hand-computing a single "-33px" left offset meant to already land the
   dot's center in the right place. That hand-computed number depended
   on this element's box-sizing (content-box, since the `*` reset
   earlier in this file doesn't reach ::before/::after) matching how
   every browser resolves width+border for an absolutely-positioned
   pseudo-element -- measured spot-on in this environment, but reported
   as visibly off-center (a few px right of the icon) in testing, which
   points at exactly that kind of cross-browser subpixel rounding
   rather than a wrong number. `translateX(-50%)` centers on the
   element's own actual rendered box at paint time in whatever engine
   is running, so it can't drift the same way.

   That got the dot centering itself right, but left one more off-by-
   one: `left` was set to land the dot's center on .roles' left BORDER
   EDGE (x=0), not on the visible line, which is the 2px border STROKE
   drawn from x=0 to x=2 -- its visual center is x=1, a pixel to the
   right. Reported again as "dots not centered on the line" once seen
   on a real screen. `-29px` (one more than the -30px above) shifts the
   target point by that same 1px, landing the dot on the stroke's
   center instead of its edge. Confirmed with a standalone repro
   (fixed geometry, no font/image variability) at 4x device-scale
   against a 1px measurement guide drawn at the exact math for the
   stroke center: the old value sits visibly left of the guide, this
   value sits on it. */
.role::before {
  content: ""; position: absolute; left: -29px; top: 1px;
  transform: translateX(-50%);
  width: 15px; height: 15px; box-sizing: border-box; border-radius: 50%;
  background: var(--accent); border: 3px solid var(--bg);
}
.role-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.role-title { font-size: 14px; font-weight: 600; }
.role-dates { font-size: 11px; color: var(--muted); }
.role ul { margin: 8px 0 0; padding-left: 18px; font-size: 13px; line-height: 1.7; color: var(--ink-soft); }
.role ul li::marker { color: var(--accent); }
.role .sublist-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 16px 0 4px; }
.role .note { font-size: 13px; color: var(--muted); font-style: italic; margin: 8px 0 0; }

.role-photos { display: flex; gap: 6px; margin-top: 16px; flex-wrap: wrap; }
.role-photo { width: 150px; height: 108px; flex-shrink: 0; position: relative; overflow: hidden; }
.role-photo.pending { width: auto; padding: 0 16px; }

.edu-row { display: flex; justify-content: space-between; align-items: baseline; padding: 16px 0; border-top: 1px solid var(--rule); flex-wrap: wrap; gap: 6px; }
.edu-row:last-child { border-bottom: 1px solid var(--rule); }
.edu-school { font-size: 14px; font-weight: 500; }
.edu-detail { font-size: 12px; color: var(--muted); }

.skill-groups { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 40px; }
@media (max-width: 700px) { .skill-groups { grid-template-columns: 1fr; } }
.skill-group h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 500; margin: 0 0 14px; }
.pills { display: flex; flex-wrap: wrap; gap: 8px; }
.pill { font-size: 12px; padding: 5px 11px; border: 1px solid var(--ink); }

.resume-cta { display: flex; align-items: center; gap: 20px; padding: 40px 0 70px; max-width: var(--content-width); margin: 0 auto; flex-wrap: wrap; }
.btn { display: inline-block; font-family: inherit; font-size: 13px; padding: 12px 22px; border: 1.5px solid var(--accent); color: var(--accent); text-decoration: none; }
.btn:hover { background: var(--accent); color: var(--bg); }
.resume-cta .note { font-size: 12px; color: var(--muted); }

/* ---------- Project detail ---------- */
.meta-row { display: flex; gap: 18px; flex-wrap: wrap; font-size: 12px; color: var(--muted); margin-bottom: 22px; }
.meta-row b { color: var(--ink); font-weight: 500; }
.tag-row { display: flex; gap: 8px; flex-wrap: wrap; }
.tag-row .pill { font-size: 11px; padding: 4px 10px; border: 1px solid var(--accent); color: var(--accent); }

.body-col { padding: 0; max-width: var(--content-width); margin: 0 auto; }
.body-col h2 {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em;
  font-weight: 500; margin: 46px 0 18px;
}
.body-col h2::before { content: "["; color: var(--accent); margin-right: 6px; }
.body-col h2::after { content: "]"; color: var(--accent); margin-left: 6px; }
.body-col p { font-size: 14.5px; line-height: 1.85; color: var(--prose); margin: 0 0 16px; }
.body-col ul, .body-col ol { font-size: 14px; line-height: 1.8; color: var(--prose); padding-left: 20px; margin: 0 0 16px; }
.body-col ul li::marker, .body-col ol li::marker { color: var(--accent); }
.body-col pre { background: var(--ink); color: var(--bg); padding: 16px 18px; overflow-x: auto; font-size: 12.5px; line-height: 1.6; margin: 0 0 20px; }
.body-col .section_break { border: none; border-top: 1px dashed var(--rule); margin: 40px 0; }

.figure-row { display: grid; grid-template-columns: 1.3fr 1fr; gap: 3px; background: var(--ink); margin: 24px 0 8px; }
@media (max-width: 700px) { .figure-row { grid-template-columns: 1fr; } }
.figure-row .photo { height: 260px; }

.links-row { display: flex; gap: 40px; padding: 36px 0 10px; max-width: var(--content-width); margin: 0 auto; flex-wrap: wrap; }
.links-row .item { font-size: 13px; }
.links-row .item .label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 6px; }

.related { padding: 50px 0 70px; max-width: var(--content-width); margin: 40px auto 0; border-top: 1px solid var(--rule); }
/* .wide is only for the writing-post layout when a TOC sidebar is
   actually rendered alongside .main-col (see .layout below) -- there,
   "Related" spans the full toc+gap+main-col width instead of just the
   text column. Every other page (including a TOC-less writing post)
   stays at the standard --content-width. */
.related.wide { max-width: 1116px; }
.related h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; margin: 0 0 20px; }
.related h2::before { content: "["; color: var(--accent); margin-right: 6px; }
.related h2::after { content: "]"; color: var(--accent); margin-left: 6px; }
.related-list { list-style: none; margin: 0; padding: 0; }
.related-list.indent { max-width: 700px; margin-left: 256px; }
@media (max-width: 900px) { .related-list.indent { margin-left: 0; } }
.related-list li { display: flex; gap: 14px; padding: 12px 0; border-bottom: 1px dashed var(--rule); align-items: baseline; flex-wrap: wrap; }
.related-list .tag { font-size: 11px; color: var(--accent); width: 70px; flex-shrink: 0; }

/* ---------- Writing post: TOC layout ---------- */
/* max-width is toc (200) + gap (56) + .main-col's standard
   --content-width (860) = 1116px, so a post WITH a TOC still gives
   .main-col its full standard width instead of squeezing it -- and
   because .layout is centered within .site-main and .main-col is
   centered within .layout, nesting two centered boxes composes: a
   TOC-less post's .main-col lands at the exact same left edge as
   every other page's --content-width column. */
.layout { display: flex; gap: 56px; align-items: flex-start; max-width: 1116px; margin: 0 auto; padding: 70px 0 0; flex-wrap: wrap; }
.toc { width: 200px; flex-shrink: 0; position: relative; top: 6px; }
.toc ul { list-style: none; margin: 0; padding: 0; }
.toc li { font-size: 12.5px; padding: 6px 0; color: var(--muted); }
.toc li a { text-decoration: none; color: inherit; }
.toc li a:hover { color: var(--accent); }
.toc li.sub { padding-left: 14px; border-left: 1px solid var(--rule); margin-left: 2px; font-size: 12px; }
/* Mobile-only collapse toggle for .toc -- hidden/inert above the phone
   breakpoint exactly like .nav-toggle-input/.nav-toggle above; the
   real mobile behavior (flex-direction column, the toggle becoming
   visible, .toc-list starting hidden) lives entirely in the mobile
   media query further down, next to .layout's own mobile fix. */
.toc-toggle-input { position: absolute; opacity: 0; pointer-events: none; }
.toc-toggle { display: none; }

/* margin: 0 auto matters even though .layout is a flex row: when the
   .toc aside isn't rendered (post.show_toc is false -- true for every
   seeded post right now), .main-col is .layout's only flex child, and a
   flex item with a max-width but no auto margins sits at flex-start
   (left) instead of centering, leaving the leftover space on one side.
   With .toc present there's no leftover space to distribute, so this is
   a no-op there -- safe in both cases. */
.main-col { flex: 1; min-width: 280px; max-width: var(--content-width); margin: 0 auto; }
.main-col .meta-row { display: flex; align-items: center; gap: 12px; font-size: 12px; color: var(--muted); margin-bottom: 18px; flex-wrap: wrap; }
.tag-pill { font-size: 11px; padding: 3px 9px; border: 1px solid var(--accent); color: var(--accent); text-transform: uppercase; letter-spacing: 0.04em; }
.main-col h1 { font-size: 30px; margin: 0; font-weight: 500; text-wrap: pretty; }
.main-col p { font-size: 15px; line-height: 1.9; color: var(--prose); margin: 0 0 20px; }
/* Round 26: inline formatting inside a paragraph (block.rich -- see
   _macros.html). strong/em lean on the browser default weight/style;
   only the in-sentence link needs an explicit rule so it reads as
   clickable against the prose color rather than inheriting it. */
.main-col p a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.main-col p a:hover { text-decoration-thickness: 2px; }
.main-col ul, .main-col ol { font-size: 15px; line-height: 1.85; color: var(--prose); padding-left: 22px; margin: 0 0 20px; }
.main-col ul li::marker, .main-col ol li::marker { color: var(--accent); }
.main-col h2 { font-size: 19px; font-weight: 600; margin: 42px 0 16px; }
.main-col h2::before { content: ""; display: inline-block; width: 7px; height: 7px; background: var(--accent); margin-right: 10px; vertical-align: middle; }
.main-col h3 { font-size: 15px; font-weight: 600; margin: 26px 0 12px; color: var(--ink-soft); }
.main-col pre { background: var(--ink); color: var(--bg); padding: 16px 18px; overflow-x: auto; font-size: 12.5px; line-height: 1.6; margin: 0 0 20px; }
/* .callout was already here, unused by any template -- .main-col
   blockquote (the block editor's new Quote block, round 26) is
   exactly the "indented, italic, accent-bordered aside" look it was
   clearly meant for, so it picks it up rather than inventing a
   second near-identical style. */
.callout, .main-col blockquote { border-left: 3px solid var(--accent); padding: 4px 0 4px 20px; margin: 30px 0; font-size: 15px; line-height: 1.8; color: var(--ink); font-style: italic; }
.figure { margin: 32px 0; }
.figure figcaption { font-size: 12px; color: var(--muted); margin-top: 8px; }

/* Round 27: content-body images and videos, rewritten to fix a real
   bug (every image was being forced into a fixed 280px-tall crop via
   the shared .photo component -- see the comment in _macros.html's
   render_blocks) and to let both size *and* shape be controlled from
   the editor: width up to the full site wrapper (not just the text
   column), height independent of width when the aspect-ratio lock is
   off, and the same resizing for video (locked to 16:9 always).

   --wpct (0-100, set inline per-block by _macros.html) is a percent
   of --wrapper-width (1320px, the same box .site-main itself is
   capped at) -- not of the narrower text column these figures
   actually sit inside. Sizing this way, rather than as a percentage
   of the immediate (860px-ish) parent, is what makes "up to the full
   site wrapper" possible at all: a plain %-width can never exceed its
   own parent. margin-left:50%+translateX(-50%) is the standard
   breakout technique -- it centers on the horizontal center of
   .img-box's nearest ancestor, but since .main-col/.body-col (that
   ancestor) is itself centered within .site-main, that center point
   IS the page's true center, so this centers correctly even when the
   box renders wider than its own parent column. min() keeps it from
   overflowing a viewport narrower than the wrapper. */
.image-figure .img-box,
.video-figure .video-embed {
  width: min(calc(var(--wpct) * var(--wrapper-width) / 100), calc(100vw - 40px));
  margin: 0;
  margin-left: 50%;
  transform: translateX(-50%);
}
@media (max-width: 900px) {
  /* Always full-width on tablet/phone regardless of the desktop
     --wpct setting -- a narrow image on a phone screen is just
     small, not "responsively" small (unchanged stance from round 26,
     just re-scoped to the new markup). */
  .image-figure .img-box,
  .video-figure .video-embed {
    width: 100%;
    margin-left: 0;
    transform: none;
  }
}

.image-figure .img-box {
  position: relative;
  overflow: hidden;
  /* var(--bg), not var(--ink) -- this is the article-body image box,
     and round 27 added support for images with real transparency
     (see media_pipeline.py's _has_real_alpha). A transparent PNG
     dropped into an article (a screenshot, a diagram) is meant to
     blend into the page's own paper color where it's see-through, not
     show a near-black rectangle behind it -- confirmed by actually
     uploading a transparent test image and looking at the rendered
     page, where var(--ink) here looked like a rendering bug even
     though the alpha channel itself was correct. .photo (the gallery/
     hero/card component elsewhere on the site) keeps var(--ink) --
     that's a legitimate loading-placeholder color for photography,
     a different use case from this one. */
  background: var(--bg);
}
/* object-fit:fill stretches the image to exactly match .img-box's
   aspect-ratio (set inline per-block above). When the block is
   aspect-locked (height_pct 100, the default), that aspect-ratio IS
   the image's own natural ratio, so the box's shape already matches
   the photo and fill/cover/contain all render identically -- no
   visible stretching happens unless the editor's lock was switched
   off and the box was deliberately given a different shape. */
.image-figure .img-box img { width: 100%; height: 100%; object-fit: fill; display: block; }
.image-figure .img-box.pending {
  display: flex; align-items: center; justify-content: center; text-align: center;
  aspect-ratio: 4 / 3; border: 1px dashed var(--rule-dashed); background: transparent;
}
.image-figure .img-box.pending span { color: var(--muted); font-style: italic; font-size: 11px; padding: 8px; }

/* Round 26: YouTube embeds (Video block). Standard responsive
   16:9-wrapper trick -- the iframe has no intrinsic size to base a
   fluid width on, so the wrapper's padding-bottom (not height) holds
   the aspect ratio and the iframe is absolutely positioned to fill it.
   Always 16:9 -- video has no aspect-lock toggle, per direct request. */
.video-embed { position: relative; padding-bottom: 56.25%; height: 0; background: var(--ink); }
.video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ---------- Photography index / album ---------- */
/* Used to be edge-margin-only (grew with the viewport, same as the
   round-nine mosaic below) -- reported as reading wider than the
   .header just above it on the same page, and asked to match. Now on
   the same --content-width standard as .header, matching how every
   other page's grid-below-a-header (.project-grid, etc.) already
   sits inside a .narrow section rather than full-bleed.

   Round 19: background was var(--rule) (a light hairline tan, meant
   to read as thin seams between cards -- same idea as .home .gallery
   before rounds 14/17 brought THAT one onto var(--bg) instead). It
   only reads as "seams" when every grid cell is filled by a card; an
   odd album count (5, here) leaves the last cell empty, and an empty
   cell just shows the grid's own background directly -- a large,
   very visible off-color rectangle, not a thin line. Switched to
   var(--bg), the same variable body itself uses and the same fix
   already applied to .gallery, so both the seams and any empty
   trailing cell read as page instead of a different color. */
.album-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; background: var(--bg); border: 1px solid var(--rule); max-width: var(--content-width); margin: 0 auto 70px; }
@media (max-width: 900px) { .album-grid { grid-template-columns: 1fr; margin: 0 24px 50px; } }
/* .album-card is now the <a> itself (round 20 feedback -- the whole
   tile should be clickable, not just the photo and the title text),
   so it needs its own display/color/text-decoration reset: without
   these it would render as an inline anchor (breaking the photo+info
   stacked layout) with the site-wide `a { text-decoration: underline }`
   painted under the title and meta text. `color: inherit` keeps the
   title reading as normal ink text rather than link-blue, matching
   how it looked when only the h3 had its own <a> before. */
.album-card { display: block; background: var(--bg); color: inherit; text-decoration: none; }
.album-card .photo { height: 220px; }
.album-card .info { padding: 16px 20px; }
.album-card h3 { font-size: 14px; margin: 0 0 6px; font-weight: 600; }
.album-card .meta { font-size: 11px; color: var(--muted); }
.empty-note { padding: 0 80px 70px; font-size: 13px; color: var(--muted); font-style: italic; max-width: 620px; }

.album-header { padding: 64px 80px 36px; display: flex; justify-content: space-between; align-items: flex-end; flex-wrap: wrap; gap: 20px; }
.album-header h1 { font-size: 34px; margin: 0 0 10px; font-weight: 500; }
.album-header .blurb { font-size: 14px; color: #57554d; max-width: 520px; line-height: 1.7; }
.meta-col { text-align: right; font-size: 12px; color: var(--muted); line-height: 1.8; }
.meta-col b { color: var(--ink); font-weight: 500; }

/* Was a fixed-row-height grid (every tile forced into the same 190px
   cell, cropped via object-fit:cover) at a width that tracked the full
   .site-main bound (effectively ~1160px) -- reported as both fighting
   photos' real proportions (panoramas and portraits alike squeezed into
   uniform cells) and simply too wide/dominant on the page. Replaced with
   a masonry layout via CSS multi-column: each photo keeps its own aspect
   ratio (no cropping at all -- .mosaic overrides the shared .photo/.photo
   img rules below to size by width with height auto, rather than the
   stretch-to-fill-cell + object-fit:cover every other use of .photo
   wants), flowing down each column and wrapping to the next, the same
   "packed masonry" look as most photo-grid apps.

   Width: originally capped at 1040px and centered, deliberately
   narrower than everything else on the page -- but that made the grid
   read as narrower than .album-header directly above it (which has
   never had a max-width of its own, so it fills the full .site-main
   width). Reported as wanting the photos to "expand out" to match the
   header instead of sitting in their own smaller column. Fixed by
   dropping the max-width/centering and reusing .album-header's own box
   model (0/80px horizontal padding, no cap) verbatim, so the two are
   pixel-for-pixel the same width rather than two numbers that happen
   to be close. This is deliberately NOT --content-width (860px, used
   by .header/.album-grid) -- .album-header was never brought onto that
   variable, so matching it means matching its actual current width,
   not the site-wide standard. If .album-header's own width ever
   changes, .mosaic should move with it. */
/* columns was a min-width (220px), so the browser packed in as many
   columns as would fit -- 4, sometimes 5 on a wide screen. Reported
   as wanting fewer, bigger tiles: a literal column *count* (3) always
   gives exactly three equal-width columns regardless of viewport,
   rather than however many happen to fit at a given minimum width. */
.mosaic {
  columns: 3;
  column-gap: 3px;
  padding: 0 80px;
  margin: 0 0 70px;
}
@media (max-width: 900px) { .mosaic { columns: 2; padding: 0 24px; } }
.mosaic a { display: block; break-inside: avoid; margin-bottom: 3px; }
.mosaic .photo, .mosaic .photo img { height: auto; }
.mosaic .photo.pending { min-height: 160px; }

/* ---------- Reading ---------- */
.now-card { display: flex; gap: 22px; align-items: center; border: 1px solid var(--rule); padding: 20px; flex-wrap: wrap; }
.cover { width: 74px; height: 108px; flex-shrink: 0; background: linear-gradient(160deg,#c9c9c4,#575754); overflow: hidden; }
.cover img { width: 100%; height: 100%; object-fit: cover; }
.now-card .title { font-size: 16px; font-weight: 600; margin: 0 0 4px; }
.now-card .author { font-size: 13px; color: #57554d; margin: 0 0 8px; }
.now-card .progress { font-size: 11.5px; color: var(--muted); }
.now-card .progress b { color: var(--accent); }

.book-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
/* Round 17: .book-row used to be four flat flex children (cover, info,
   an optional tag-pill, an optional .stars) with flex-wrap as the only
   concession to narrow widths -- which item wrapped onto its own line
   depended on whether that particular row had a tag, a rating, both,
   or neither, so every row in the list ended up a different shape
   ("still wonky" on a phone). Grouping cover+info into one .main and
   tag+stars into one .meta (see reading.html) gives every row exactly
   two pieces regardless of which optional badges it has -- .main
   always fits on the row's first line (it's the only flex-growing
   item), and .meta either sits inline next to it (desktop, plenty of
   width) or wraps as one consistent unit below it (mobile, below). */
.book-row { display: flex; align-items: center; gap: 20px; padding: 18px 0; border-bottom: 1px solid var(--rule); flex-wrap: wrap; }
.book-row .main { display: flex; align-items: center; gap: 20px; flex: 1; min-width: 160px; }
.book-row .cover { width: 50px; height: 74px; flex-shrink: 0; }
.book-row .info { min-width: 0; }
.book-row .title { font-size: 15px; font-weight: 500; margin: 0 0 4px; }
.book-row .author { font-size: 12.5px; color: var(--muted); }
.book-row .meta { display: flex; align-items: center; gap: 20px; flex-shrink: 0; }
.stars { display: flex; gap: 2px; flex-shrink: 0; }
.stars svg { width: 15px; height: 15px; }
.tag-pill.book { font-size: 10.5px; padding: 3px 9px; border: 1px solid var(--rule-dashed); color: var(--muted); flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.03em; }

/* ---------- Reading: sort control ---------- */
/* Same pill-link look as .tag-filter (the Writing index's tag filter)
   rather than a new style -- this is the same "list of mutually
   exclusive view options" pattern, just sorting instead of filtering.
   .tag-filter gets its centering for free by sitting inside a
   section.block.narrow in writing_index.html; .sort-filter sits
   directly in the page (it has to span both the Finished and Didn't
   Finish sections below it), so unlike .tag-filter it needs its own
   max-width/auto-margin -- without it this rendered flush against the
   left edge of the viewport instead of lining up with the rest of the
   page's --content-width column (round 17 feedback: sort control
   verification). */
.sort-filter { display: flex; align-items: center; gap: 10px; max-width: var(--content-width); margin: 0 auto 24px; font-size: 11px; color: var(--muted); }
.sort-filter .label { text-transform: uppercase; letter-spacing: 0.05em; }
/* Round 17 verification caught this missing: without its own `a`/
   `a.current` rules, .sort-filter's links fell through to the global
   `a { color: var(--ink); text-decoration: underline; }` -- both links
   looked identical (or worse, one looked "picked" simply because a
   browser paints a just-clicked link's :visited state differently),
   so "current" carried no actual style. Copied from .tag-filter a /
   a.current so the active sort reads the same way an active tag does. */
.sort-filter a { text-decoration: none; color: var(--muted); }
.sort-filter a:hover { color: var(--accent); }
.sort-filter a.current { color: var(--accent); font-weight: 500; text-decoration: underline; text-underline-offset: 3px; }

/* ---------- Contact ---------- */
/* No max-width/auto-margin of its own: this list already lives inside
   section.block.narrow (--content-width, 860px). Giving it a narrower
   width of its own re-centered it inside that box, so "Email"/"LinkedIn"
   sat indented relative to the "Get in touch." heading above -- the
   exact left-edge mismatch reported against the About page. */
.contact-methods { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
.contact-methods li { display: flex; justify-content: space-between; align-items: baseline; padding: 18px 0; border-bottom: 1px solid var(--rule); gap: 16px; flex-wrap: wrap; }
.contact-methods .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.contact-methods a { font-size: 15px; }

/* ---------- Lightbox (fullscreen dark viewer) ---------- */
.lightbox {
  position: fixed; inset: 0; z-index: 1000; display: none;
  background: #15161a; color: #e9e6df;
  font-family: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.lightbox.open { display: block; }
.lb-topbar { position: absolute; top: 0; left: 0; right: 0; display: flex; justify-content: space-between; align-items: center; padding: 26px 40px; z-index: 3; }
.lb-counter { font-size: 12px; color: #9a968c; letter-spacing: 0.04em; }
.lb-close { width: 34px; height: 34px; border: 1px solid #3a3b40; display: flex; align-items: center; justify-content: center; cursor: pointer; background: none; color: inherit; }
.lb-close svg { width: 14px; height: 14px; }
.lb-arrow { position: absolute; top: 50%; transform: translateY(-50%); width: 46px; height: 46px; border: 1px solid #3a3b40; display: flex; align-items: center; justify-content: center; z-index: 3; cursor: pointer; background: none; }
.lb-arrow.left { left: 30px; } .lb-arrow.right { right: 30px; }
.lb-arrow svg { width: 16px; height: 16px; stroke: #e9e6df; }
.lb-arrow:hover { border-color: var(--accent); }
.lb-arrow:hover svg { stroke: var(--accent); }
.lb-frame { position: absolute; top: 90px; left: 140px; right: 140px; bottom: 200px; overflow: hidden; display: flex; align-items: center; justify-content: center; }
.lb-frame img { max-width: 100%; max-height: 100%; object-fit: contain; }
@media (max-width: 900px) { .lb-frame { left: 20px; right: 20px; bottom: 160px; } }
.lb-meta-bar { position: absolute; left: 140px; right: 140px; bottom: 128px; display: flex; justify-content: space-between; align-items: flex-end; z-index: 3; flex-wrap: wrap; gap: 10px; }
@media (max-width: 900px) { .lb-meta-bar { left: 20px; right: 20px; bottom: 110px; } }
.lb-meta-bar .caption { font-size: 14px; color: #f2efe8; }
.lb-meta-bar .sub { font-size: 11.5px; color: #9a968c; margin-top: 4px; }
.lb-filmstrip { position: absolute; left: 0; right: 0; bottom: 0; height: 96px; display: flex; align-items: center; gap: 8px; padding: 0 40px; overflow-x: auto; background: linear-gradient(to top, rgba(0,0,0,0.4), transparent); }
.lb-thumb { width: 76px; height: 56px; flex-shrink: 0; position: relative; overflow: hidden; opacity: 0.55; cursor: pointer; border: none; padding: 0; background: none; }
.lb-thumb img { width: 100%; height: 100%; object-fit: cover; }
.lb-thumb.active { opacity: 1; box-shadow: 0 0 0 2px var(--accent); }

/* ---------- misc utility ---------- */
.mono-note { font-size: 12px; color: var(--muted); font-style: italic; }

/* =========================================================
   Mobile (phone-width). One consolidated section rather than a
   one-liner @media scattered after each rule (the pattern used
   elsewhere in this file, e.g. .project-grid/.skill-groups/.mosaic)
   because this is a single cross-cutting fix, not a series of
   unrelated per-component tweaks: reported as "doesn't handle a
   phone screen well," and the reason turned out to be the same bug
   repeated across most of the page rather than several different
   ones.

   The bug: .header, .hero, section.block.narrow, .layout, .related,
   .links-row, .resume-cta and .about-methods' containing header all
   pair a max-width with `margin: 0 auto` but carry ZERO horizontal
   padding of their own -- correct once the viewport is wider than
   that max-width (nothing to pad, the auto margins do the centering),
   but on a phone the viewport is narrower than the max-width, so
   those auto margins collapse to 0 and the content runs edge-to-edge
   into the screen bezel. (Contrast section.block WITHOUT .narrow,
   .nav, .footer, .album-header and .empty-note, which already carry
   a flat 80px of horizontal padding always -- correct at desktop
   width, just too much of it once the viewport itself is only
   ~350-400px.) Both problems get the same fix below: give every one
   of them a sane ~20px of horizontal padding at phone width. Because
   of the global `* { box-sizing: border-box }` at the top of this
   file, adding padding-left/right here can't push any of these past
   their own max-width -- safe to do as a flat additive rule. */
@media (max-width: 640px) {
  .nav { padding: 18px 20px; position: relative; }
  .footer { padding: 20px 20px 32px; }

  /* Phone nav: was every link inline, wrapping onto a second (and on
     the narrowest phones a third) row under the wordmark -- functional
     but visually heavy for what's meant to be a one-line header.
     Collapses to a wordmark + a bracketed "[ menu ]" toggle (matching
     the "[ Section ]" bracket motif used elsewhere) on one row; the
     full link list drops down below as its own block only once opened.
     Pure CSS checkbox hack -- see the base (non-mobile) rules above
     for why the input/label are otherwise hidden and out of the way. */
  .nav-toggle {
    display: inline-block;
    font-size: 13px;
    letter-spacing: 0.02em;
    color: var(--ink);
    cursor: pointer;
    padding: 4px 0;
  }
  .nav-toggle-close { display: none; }
  .nav-toggle-input:checked ~ .nav-toggle .nav-toggle-open { display: none; }
  .nav-toggle-input:checked ~ .nav-toggle .nav-toggle-close { display: inline; }

  .navlinks {
    display: none;
    flex-direction: column;
    flex-basis: 100%;
    gap: 0;
    margin-top: 18px;
    padding-top: 4px;
    border-top: 1px solid var(--rule);
  }
  .nav-toggle-input:checked ~ .navlinks { display: flex; }
  .navlinks a { padding: 12px 0; border-bottom: 1px dashed var(--rule); background-image: none !important; }
  .navlinks a:hover, .navlinks a.current { color: var(--accent); }
  /* The middot separators only make sense inline between links on one
     line -- stacked one-per-row they'd just be an orphan glyph under
     each link, so hide them here rather than reworking the markup. */
  .navlinks span { display: none; }

  .header,
  .hero,
  section.block,
  section.block.narrow,
  .layout,
  .related,
  .links-row,
  .resume-cta,
  .album-header,
  .empty-note,
  .body-col,
  .sort-filter {
    padding-left: 20px;
    padding-right: 20px;
  }
  .header { padding-top: 48px; padding-bottom: 30px; }
  .album-header { padding-top: 40px; padding-bottom: 28px; }

  /* Mobile TOC. .layout is a flex ROW (.toc + .main-col) at every
     width, sized by .toc's fixed 200px plus .main-col's flex:1 --
     min-width:0 (below) lets .main-col flex-shrink down to whatever
     is left instead of forcing the row to wrap, so on a phone the two
     never actually stack: they width-share a ~335px viewport, the
     article title gets crammed into an 80-100px sliver, and ordinary
     (unhyphenated) word-wrapping just lets it overflow past that
     sliver and off the right edge of the screen. Reported as "the
     page layout doesn't look great with the table of contents."
     Reproduced with a real note that has show_toc on -- every seeded
     note has it off, which is exactly why no earlier mobile pass ever
     hit this.
     Fix, in two parts, the second one a direct choice from three
     offered (stack it open above the title / move it below the title
     / collapse behind a toggle -- collapse was picked): stack .layout
     into a column instead of squeezing the row, and hide .toc-list
     behind the same bracketed [ ... ] checkbox-hack toggle the mobile
     nav already uses, so the TOC doesn't push the title down at all
     unless a reader actually opens it. */
  .layout { flex-direction: column; gap: 0; }
  .toc { width: 100%; top: 0; border-bottom: 1px solid var(--rule); padding-bottom: 14px; margin-bottom: 22px; }
  .toc-heading { display: none; }
  .toc-toggle { display: inline-block; font-size: 13px; letter-spacing: 0.02em; color: var(--ink); cursor: pointer; padding: 2px 0; }
  .toc-toggle-close { display: none; }
  .toc-toggle-input:checked ~ .toc-toggle .toc-toggle-open { display: none; }
  .toc-toggle-input:checked ~ .toc-toggle .toc-toggle-close { display: inline; }
  .toc-list { display: none; margin-top: 14px; }
  .toc-toggle-input:checked ~ .toc-list { display: block; }

  /* .hero was a flex ROW at every width -- fine on desktop, where
     .hero-text's flex-basis (flex: 1 -> 0%) and .hero-photo's fixed
     320px both have room to sit side by side. On a phone the two no
     longer fit side by side, but they were never wrapping onto
     separate lines either: .hero-photo's width:100% (capped at
     max-width:320px, see below) plus the 28px gap already summed to
     just under the padded content width, so flex-wrap never triggered
     a new line -- .hero-text was just left ~0-20px to grow into,
     which wrapped its heading and paragraphs one word (sometimes one
     letter) per line while .hero-photo sat on top of/beside that
     sliver, reported as "the hero photo and text look very bad."
     flex-direction: column sidesteps the fragile width arithmetic
     entirely -- text first (unchanged reading order), photo below. */
  .hero { flex-direction: column; padding-top: 48px; padding-bottom: 24px; gap: 28px; }

  /* Flex items with a min-width wider than the padded viewport force
     the whole row to overflow instead of shrinking (that's what
     min-width is for) -- on the narrowest phones, .hero-text's 320px
     and .main-col's 280px are both wider than a ~335px content area
     (375px viewport - 20px padding * 2). min-width:0 lets them
     actually shrink to fit; flex:1 already makes them grow back to
     fill the row on every wider screen, so this only matters exactly
     where the overflow would otherwise happen. */
  .hero-text, .main-col, .toc { min-width: 0; }

  /* .hero-photo was a fixed 320x400 -- fine once .hero has padding
     (above), but a fixed px width can still be wider than the
     content area on the smallest phones. width:100% with a max-width
     cap keeps it fluid down to the smallest screens; aspect-ratio
     (rather than the desktop rule's fixed height) is what keeps the
     4:5 portrait proportion while the width itself changes. */
  .hero-photo { width: 100%; max-width: 320px; }
  .hero-photo .photo { height: auto; aspect-ratio: 4 / 5; }

  /* .writing-list li is a baseline-aligned flex ROW (tag, title,
     date) with flex-wrap as the only concession to narrow widths --
     which wraps a different item onto line 2 depending on how long
     that particular title happens to be (a short title keeps tag+
     title+date together and wraps nothing; a long one wraps the date
     down; a very long one wraps the title itself, which then pushes
     the date to a third position), so every row ends up stacked
     differently from its neighbors. Reported as "everything kind of
     jumbled." A column stack gives every row the exact same three-line
     shape -- tag, then title, then date -- regardless of length. */
  .writing-list li { flex-direction: column; align-items: flex-start; gap: 4px; }
  .writing-list .tag { width: auto; }

  /* Round 17: same "every row wraps a different shape" bug as
     .writing-list above, on the reading list's .book-row instead --
     see the comment by .book-row's own rules for the .main/.meta
     split this depends on. .main (cover+info) already fits the row's
     first line on its own at any width; forcing .meta (tag+stars) onto
     its own full-width line, indented to align under the title rather
     than under the cover, is what makes every row -- with a tag, a
     rating, both, or neither -- read as the same shape. */
  .book-row .meta { flex-basis: 100%; justify-content: flex-start; padding-left: 70px; margin-top: 2px; }

  /* .gallery's fixed 420px height (sized for the desktop 1-large/
     2-small arrangement) squeezed all three photos into short, wide
     slivers once the grid itself narrowed to phone width. Stacking
     them in one column, each at its own sensible height, keeps every
     photo actually recognizable instead of preserving a desktop
     layout that was never asked to survive at this width. */
  .gallery { grid-template-columns: 1fr; grid-template-rows: none; height: auto; }
  .gallery .photo.large { grid-row: auto; }
  .gallery .photo { height: 220px; }

  /* Tighter than the >900px tablet breakpoint's 2 columns (still set
     above) -- at phone width even 2 columns leaves each mosaic tile
     too narrow to read. This rule and the >900px one never conflict:
     a phone matches both media queries, and this one, being later in
     the file, wins. */
  .mosaic { columns: 1; padding: 0 16px; }

  /* Lightbox chrome was sized for desktop (40px top/side padding,
     30px arrow insets) -- harmless at that width but eats real space
     on a phone screen, which the >900px rule above doesn't reduce. */
  .lb-topbar { padding: 16px 16px; }
  .lb-arrow.left { left: 8px; } .lb-arrow.right { right: 8px; }
  .lb-arrow { width: 38px; height: 38px; }
  .lb-filmstrip { padding: 0 16px; }
}
