/* ============================================================
   ABOUT, page-specific layout. V2: Swiss field on a 12-column
   canon grid, translated from Baha's diagonal armature. Tokens
   only (Protocol 3). Loaded after tokens.css + styles.css on
   about.html; the home build (index.html / styles.css) is untouched.
   Zebrikitou, 2026-06-26.
   ============================================================ */

/* --- The canon grid ----------------------------------------------------------
   Baha composed against a diagonal armature (corner-to-corner and
   corner-to-midpoint diagonals, the classical golden canon). That armature
   lives on a FIXED rectangle; the web reflows, so it cannot ship literally.
   Its intersection points fall on the phi / thirds lines anyway, so we honor
   the composition by snapping each element to a 12-column line grid the browser
   can hold across breakpoints. Same picture, responsive and reproducible.
   Per-section placement classes set grid-column; gutter and rhythm are tokens. */
.canon {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--grid-gutter);
  row-gap: var(--space-section);
  align-items: start;
}
/* Each band is a single row. Pin every child to row 1 so a later-DOM item placed
   in an earlier column does not get bumped to the next row by sparse
   auto-placement (the cause of the misaligned media in spark + customization).
   Reset to auto on mobile, where the grid collapses to one column. */
.canon > * { grid-row: 1; }

/* --- Media frame (greybox until real assets land) ---
   Rounded + hairline + surface fill, same family as the home project cards
   (Baha's call: "make them round like the ones on the home page"). Drop an
   <img> in to replace the .about-media__ph label. Per-figure aspect via the
   --ratio knob; in the dark zone it auto-themes through the token layer. */
.about-media {
  aspect-ratio: var(--ratio, 4 / 3);
  border-radius: var(--radius-card);
  border: 1px solid var(--border);
  background: var(--surface);
  overflow: hidden;
  display: grid;
  place-items: center;
}
.about-media img { width: 100%; height: 100%; object-fit: cover; }
.about-media__ph {
  font-size: var(--fs-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--fg-muted);
}

/* --- Shared text bits (one vocabulary across every band) --- */
.about-eyebrow { margin-bottom: var(--space-24); }    /* uses .eyebrow */
.about-lede { font-size: var(--fs-lead); line-height: 1.6; }
.about-lede + .about-lede { margin-top: var(--space-16); }
.about-body { max-width: 52ch; }                       /* reading measure */

/* --- Uniform section rhythm -------------------------------------------------
   styles.css pads .section by a full --space-section top AND bottom, so adjacent
   bands sat 2x apart while the hero (padding-bottom:0) sat only 1x from Spark.
   Halve each side here so every section-to-section gap resolves to exactly one
   --space-section, dark band included. About-page scope only; the home build
   keeps its own rhythm. Baha, 2026-06-26. */
.section { padding-block: calc(var(--space-section) / 2); }

/* ============================================================
   HERO: statement (cols 1-4) + two portraits (6-9, 10-12)
   The two portraits now sit flush as a pair on the right; the one open
   track is col 5, between the text block and the portrait pair.
   B is the narrower, taller crop = the stagger Baha drew. Tune the stagger
   with each figure's --ratio, never a px.
   ============================================================ */
.about-hero { padding-top: clamp(48px, 9vw, 128px); padding-bottom: calc(var(--space-section) / 2); }
.about-hero__text { grid-column: 1 / 5; }
.about-hero__pa   { grid-column: 6 / 10; }

/* Right column = portrait B with the resume link beneath it. The column stretches
   to portrait A's height so the link can drop onto A's bottom line. */
.about-hero__pcol { grid-column: 10 / 13; align-self: stretch; display: flex; flex-direction: column; }
.about-hero__pcol .about-hero__pb { flex: 0 0 auto; }   /* hold the 2/3 crop height */
/* Resume link: gray at the main-text size, like the header sub (.topbar__sub),
   no button chrome. margin-top:auto drops it to the bottom of the stretched
   column (onto portrait A's bottom line); align-self flex-end flushes it to
   portrait B's right edge. Baha, 2026-06-26. */
.about-hero__resume {
  margin-top: auto;
  align-self: flex-end;
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  font-size: var(--fs-lead);
  color: var(--fg-muted);
  text-align: right;
}
.about-hero__resume:hover { color: var(--fg); }
.about-hero__resume .download { width: 1em; height: 1em; transition: transform .2s ease; }
.about-hero__resume:hover .download { transform: translateY(2px); }

/* Display statement, hero opener only (Baha: match "I tell stories ..." in size). */
.about-hero__statement {
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}
/* Decades closer now drops to the home project-description register
   (.project__desc: fs-h2 / lh-heading / regular). Baha's call, 2026-06-26:
   the vow reads as a statement, not a second hero. */
.about-decades__statement {
  font-size: var(--fs-h2);
  line-height: var(--lh-heading);
  font-weight: var(--weight-regular);
  text-wrap: balance;
}
.about-hero__statement + .about-lede { margin-top: var(--space-32); }

/* ============================================================
   WHERE DO I COME INTO PLAY: an adapted Norman action-cycle diagram (left) and
   the section title (right), mirroring the hero above (there text-left / picture
   -right; here picture-left / text-right). The figure is inline SVG on a gray
   card (--surface-sunken, the house panel gray) with black boxes and black
   connectors; every colour and type size is a token (Protocol 3). SVG text is in
   viewBox units, so it scales with the figure. Baha, 2026-07-25.
   ============================================================ */
/* cols 1-5 bled one gutter, so the card's right edge lands on the LEFT edge of
   the hero's first portrait above (about-hero__pa starts at col 6). Same bleed
   idiom as .about-custom__text; the mobile reset lives in the 810 block. */
.about-play__fig  { grid-column: 1 / 6; margin: 0; margin-right: calc(-1 * var(--grid-gutter)); }
.about-play__text { grid-column: 8 / 13; align-self: start; }    /* cols 8-12, top-aligned with the card */
.about-play__title {
  font-size: var(--fs-display);        /* peer of the hero statement */
  line-height: var(--lh-display);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  /* line break is set explicitly in the markup ("Where do I come" / "into play?"), so no text-wrap: balance */
}
.about-play__title + .about-lede { margin-top: var(--space-32); }  /* same gap the hero sets under its statement */
.about-play__credit { margin-top: var(--space-16); text-align: left; }  /* eyebrow credit, under the left edge of the illustration */

/* The SVG diagram (.wf = the "what I do" workflow). A graded near-monochrome on the
   rounded gray field (--surface-sunken): VISION/WORLD bars darkest, the four question
   boxes a step lighter, the dashed field in --grey-400 (the lightest gray that still
   clears 3:1 non-text contrast on the card). The flow arrows are white-bodied with a
   soft 1px --grey-250 outline (light, well short of the boxes). Box text is white; the
   "what I'm in charge of" callout + its
   pointer stay ink, the one dark focal accent. Every tone is a token (Protocol 3). */
.wf { display: block; width: 100%; height: auto; font-family: var(--font-sans); }
.wf__bg    { fill: var(--surface-sunken); }      /* the gray field (house panel gray) */
.wf__bar   { fill: var(--grey-700); }            /* VISION / WORLD bars, darkest tone */
.wf__box   { fill: var(--grey-500); }            /* the four question boxes, a step lighter than the bars */
.wf__label { fill: var(--white); font-size: 26px; font-weight: var(--weight-regular); letter-spacing: 1px; dominant-baseline: central; }  /* central = vertically centred on its y */
.wf__q     { fill: var(--white); font-size: 21px; font-weight: var(--weight-regular); dominant-baseline: central; }
.wf__accent { font-family: var(--font-serif); font-style: italic; font-size: 0.92em; }  /* Baskerville, pulled back; inherits the white box text */
.wf__flow  { fill: var(--white); stroke: var(--grey-250); stroke-width: 1; vector-effect: non-scaling-stroke; }   /* white arrow body, soft 1px --grey-250 outline (light, well short of the boxes); non-scaling so it stays 1px at any figure size */
.wf__dash  { fill: none; stroke: var(--grey-400); stroke-width: 2.5; stroke-dasharray: 6 7; }
.wf__callout { fill: var(--ink); font-family: var(--font-serif); font-style: italic; font-size: 24px; }  /* the annotation stays ink, the one dark focal element */
.wf__point { stroke: var(--ink); stroke-width: 2.5; fill: none; }  /* thin pointer to the dashed field (part of the ink annotation) */
.wf__point-fill { fill: var(--ink); }

/* ============================================================
   STORY DIVIDER: the home page's project hairline (border-top: 1px solid
   var(--border)) closes the tool-stack coda above and opens the biographical
   narrative below. No top padding on the block: the tool-stack's own bottom
   padding is the space above the line, and the spark's top padding carries the
   gap down to the Vice City images. Baha, 2026-07-25.
   ============================================================ */
.about-rule {
  border: 0;
  border-top: 1px solid var(--border);   /* same hairline as the home project rows */
  margin: 0;
}

/* ============================================================
   THE SPARK: thumbnails on the canon via subgrid (cols 1-4 and 5-6)
   + text (cols 8-11). Subgrid makes the two pictures snap to the SAME
   12 tracks as everything else, instead of a private half-and-half
   split, so every width below is a real count of canon columns.
   ============================================================ */
.about-spark__thumbs {
  grid-column: 1 / 7;                 /* span canon cols 1-6 */
  display: grid;
  grid-template-columns: subgrid;     /* adopt the canon's own column tracks */
  column-gap: var(--grid-gutter);
  align-items: start;
}
.about-spark__thumb-a { grid-column: 1 / 5; }  /* cols 1-4, four wide */
.about-spark__thumb-b { grid-column: 5 / 7; }  /* cols 5-6, now flush against thumb-a */
.about-spark__text { grid-column: 8 / 12; }    /* cols 8-11, four wide */

/* ============================================================
   CUSTOMIZATION: text (cols 1-4, bled one gutter) + image (cols 8-12)
   The text's right edge lands on the LEFT edge of the second spark picture above
   (thumb-b starts at col 5). A `1 / 5` item stops at the TRAILING edge of col 4
   (= the first picture's right edge); thumb-b's left edge sits one gutter
   further right, and a from-col-1 item can't name that line, so we bleed the
   cell's right edge out by one --grid-gutter to reach it. Baha, 2026-07-25:
   align to the second picture's START, not the first's end. Image five wide from
   col 8; the old 60ch cap is dropped (the bled cell is the measure now).
   ============================================================ */
.about-custom__text  { grid-column: 1 / 5; margin-right: calc(-1 * var(--grid-gutter)); }
.about-custom__media { grid-column: 8 / 13; }

/* ============================================================
   DECADES (dark): text (cols 1-4) + inset portrait (cols 8-11)
   The body paragraphs resolve, then a small space, then the closing vow at
   the home project-description size (see .about-decades__statement above).
   The portrait now starts at col 8 to share a left edge with the Customization
   image directly above it (also col 8). Baha, 2026-07-25.
   ============================================================ */
.about-decades__text  { grid-column: 1 / 5; }
.about-decades__media { grid-column: 8 / 12; }
.about-lede + .about-decades__statement { margin-top: var(--space-32); }

/* ============================================================
   THE LEAP(S) OF FAITH (white): closing reflection. Text left on the canon,
   its right edge bled to the hero portrait's left edge (col 6); on the right, a bird on a bare branch with a
   reflective quote beneath it. The image is a transparent silhouette, so it
   FLOATS, with no greybox frame (that card chrome would crop a cutout and fight
   its edges; Protocol 2). It now spans six canon columns (two wider than the
   Decades portrait above), then slips off the right margin to kiss the extreme
   right of the page. Baha, 2026-06-30.
   ============================================================ */
.about-leap { overflow-x: clip; }   /* contain the right-edge bleed; no h-scrollbar */
/* cols 1-5 bled one gutter so the measure's right edge lands on the LEFT edge of
   the hero's first portrait (leading edge of col 6), same idiom as
   .about-custom__text. The old 62ch cap is dropped so the line reaches that edge
   (a wider measure than before, by Baha's call). Baha, 2026-07-25. */
/* V4.37, Baha: narrowed one column, from `1 / 6` to `1 / 5`, so this paragraph
   sits at exactly the measure of the "Customization and modding" text above it
   (.about-custom__text, same span, same one-gutter bleed). Two left-hand text
   blocks on the same page reading at two different widths was the tell. */
.about-leap__text { grid-column: 1 / 5; margin-right: calc(-1 * var(--grid-gutter)); }

/* Cols 7-12, right edge bled to the viewport's extreme right. position:relative +
   a negative `right` (NOT transform, which is owned by the [data-reveal] fade)
   slides it rightward by the exact distance from the content edge to the
   viewport edge: the container's right padding, plus half of any overflow once
   the viewport passes the 1760 cap and the container stops growing and centres.
   Pure tokens.

   V4.37, Baha: "make the bird larger, it should start at the ending of the
   player.bmp line." That line is grid line 7, where .about-spark__thumb-b (the
   collage of edited texture files, i.e. the player.bmp picture, cols 5-6) ends.
   The bird's CELL already began there, but the rightward bleed above then pushed
   its visible left edge one --leap-bleed further right, so it never actually
   landed on that line. Cancelling the bleed on the left with an equal negative
   margin puts the left edge back on line 7 while the right edge stays pinned to
   the viewport, which is what makes the picture bigger: it grows by exactly the
   bleed, ~115px at 1440 (583 -> 698 wide, +20%).

   The bleed is a single custom property now because it is consumed twice and the
   two MUST stay identical: if they drift, the left edge stops landing on line 7.

   Why justify-self:end + width rather than a negative margin-left, which would
   also work: past --container-max the container centres, so --leap-bleed keeps
   growing with the viewport and the picture would grow with it, unbounded. It
   hits 1256px at 2560 and 1696px at 3440, upscaling a 1047px source into a soft
   billboard. Pinning the END edge means the box grows LEFTWARD off its own right
   edge, which lets --leap-max simply cap it: the right edge stays welded to the
   viewport at every width, and only the left edge gives, easing off line 7 on
   ultrawide instead of running away. Below ~2150px the cap never engages and the
   alignment is exact. */
.about-leap__media {
  --leap-bleed: calc(var(--pad-x) + (100vw - min(100vw, var(--container-max))) / 2);
  --leap-max: 1047px;               /* the source PNG's native width: never upscale it */
  grid-column: 7 / 13;
  align-self: end;                  /* land the aside (image + quote) on the text's bottom line */
  justify-self: end;                /* pin the RIGHT edge; all growth happens leftward */
  width: calc(100% + var(--leap-bleed));   /* left edge lands on grid line 7 */
  max-width: var(--leap-max);
  position: relative;
  right: calc(-1 * var(--leap-bleed));     /* then slide the whole thing to the page edge */
}
.about-leap__media img { width: 100%; height: auto; }   /* cutout keeps its natural ratio */

/* The reflective quote sits UNDER the bird, in the serif italic accent (the
   site's quote voice, e.g. the footer). Left-aligned to the image edge but held
   to a readable measure and kept one gutter off the bled page edge, so the line
   never runs into the margin the picture is allowed to cross. Baha, 2026-06-30. */
.about-leap__quote {
  margin-top: var(--space-24);
  max-width: 46ch;
  padding-right: var(--grid-gutter);
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--fs-lead);
  line-height: 1.5;
  color: var(--fg);
  text-wrap: pretty;
}

/* ============================================================
   PIECES OF ME: the vinyl wall
   The page's closing coda, under "The leap(s) of faith". A label rides ABOVE
   its own hairline (Baha's order, and the right one: the rule then reads as
   this section's opening edge, not the previous section's closing one), and the
   left half carries the statement with the record wall beneath it. Cols 7-12
   are deliberately empty, the right half is the next piece of work.

   The covers are FILES, not live Discogs calls: their CDN sets a __cf_bm cookie
   and a decorative coda should not sit on a third party's uptime. Refresh the
   mirror with `python3 KB/sync-vinyl.py` (see that file's header).
   ============================================================ */
/* No padding override here on purpose. about.css halves .section's padding to
   --space-section/2 per side, so this band's own top padding plus .about-leap's
   bottom padding resolve to exactly ONE --space-section above the eyebrow, the
   same gap as every other section boundary on the page. (The .about-story
   divider higher up DOES zero its padding, but that is a bare <section> with no
   .section class and therefore no padding on either side. Copying it here would
   have made this the one boundary sitting at half the page's rhythm.) */
.about-vinyl__label { margin-bottom: var(--space-16); }   /* label → rule, a tight pair */

/* TWO ROWS, ONE GRID, and that is the whole trick of this band.

   Baha, 2026-08-01: the two headings should sit on the same bottom line, and the
   two containers under them should align at the top AND at the bottom. None of
   that is achievable with two independent half-wrappers, because each would size
   its own rows and the left heading runs to two lines while the right runs to
   one. So the halves are dissolved: the four pieces (two headings, two
   containers) are direct children of this one grid.

   Row 1 is as tall as the TALLER heading, and both headings sit on its bottom
   edge (align-self: end), which is exactly "align to the bottom line of 'of my
   favorite albums'" without measuring a single line height. Row 2 is as tall as
   the taller container and both stretch into it, so the gray boxes start and end
   on the same two lines. Nothing is hard-coded; if the left heading ever wraps to
   three lines, or a book is added, both sides re-agree by themselves.

   row-gap replaces the two separate top margins the containers used to carry.
   That is not tidying: identical heading-to-container spacing on both sides is
   what makes the top edges land together once the headings are bottom-aligned. */
.about-vinyl__body {
  margin-top: clamp(40px, 5vw, 72px);        /* rule → headings */
  row-gap: clamp(48px, 5vw, 80px);           /* headings → containers, both halves */
}
.about-vinyl__body > .about-vinyl__title,
.about-vinyl__body > .about-books__title    { grid-row: 1; align-self: end; }
.about-vinyl__body > .about-vinyl__panel,
.about-vinyl__body > .about-books__shelves  { grid-row: 2; align-self: stretch; }

/* The left half runs cols 1-6, heading and panel alike. The subgrid that used to
   wrap them is gone: it existed so the stack could name `grid-column: 1 / 5` and
   sit at exactly the Vice City picture's width, and the stack is inside the panel
   now, so there is nothing left for it to do. */
.about-vinyl__title { grid-column: 1 / 7; }

/* The gray panel: the stack and the Discogs link in one container.

   WIDTH: cols 1-6, which is .about-spark__thumb-a (1 / 5) plus
   .about-spark__thumb-b (5 / 7), i.e. BOTH Vice City pictures combined, edge to
   edge (Baha, 2026-08-01, superseding "the width of the left picture"). Real
   canon lines, not a derived percentage, so the panel's edges and the pictures'
   edges cannot drift.

   It is a grid, one column, for two reasons: grid blockifies the link's
   inline-flex, so `justify-self: start` below still governs it and there is no
   anonymous line box adding stray leading under the stack; and align-content:
   start means that when this box is stretched to match the shelves opposite, the
   extra height lands as space at the BOTTOM of the panel rather than being
   dealt out into the stack and pulling the covers apart.

   Fill, radius and padding are lifted wholesale from .about-books__shelf. Same
   values, deliberately: the two halves of "Pieces of me" should read as one
   family of containers, not as two separately-invented panels. */
.about-vinyl__panel {
  grid-column: 1 / 7;
  display: grid;
  align-content: start;
  background: var(--surface-sunken);
  border-radius: var(--radius-panel);
  padding: clamp(20px, 2.2vw, 32px);
}
.about-vinyl__link { justify-self: start; }   /* start: the panel's grid would otherwise stretch the link full width */

.about-vinyl__title {
  /* Same voice as the tool stack and the hero, via the shared role token. */
  font-size: var(--fs-statement);
  line-height: var(--lh-display);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;   /* no <br> here: the line is long enough that a fixed break would strand a word at some widths */
}

/* The wall: 35 sleeves butted flush into ONE STACK, fading out at the bottom.

   The block reads as one object, a mass of records, with an even gutter on both
   axes (Baha, 2026-08-01: the gap goes back INSIDE the stack, horizontally and
   vertically). One uniform gap, not the earlier tight-columns/wide-rows split:
   an even gutter keeps it reading as a single grid rather than as separate
   shelves. The fade then says the mass continues past what is drawn, and the
   Discogs link under it says where.

   The covers stay square (border-radius: 0) and keep their hairline. Both were
   load-bearing at gap 0 and are cheap to keep now: square is honest to a sleeve,
   and the hairline holds one dark cover apart from the next.

   HISTORY, so nobody rebuilds the bug: this first shipped as a clever
   `repeat(auto-fill, minmax(max(44px, calc(100%/10 …)), 1fr))` that derived its
   own column count so the covers could never fall below a legible floor. It is
   invalid at computed-value time, so grid-template-columns fell back to its
   INITIAL value (none), the items auto-placed into one implicit column, and
   every cover rendered as a ~580px square stacked 68 deep. Do not reintroduce
   it. This is now the exact idiom .toolstack__tiles already proves on this
   site: a fixed count, minmax(0, 1fr) tracks, and the square declared on the
   cell.

   30 records fill exactly 5 rows of 6, no short last row, which is what the
   panel wants: inside a container the ragged edge reads as an accident rather
   than as a shelf. */
.about-vinyl__wall {
  list-style: none; padding: 0;
  margin: 0;   /* the panel owns the spacing now */
  display: grid;
  /* minmax(0, 1fr), not 1fr: 1fr floors each track at the image's intrinsic
     300px, which would blow the row far past its column. Same trap the tool
     stack documents. THE KNOB is the 6 (history: 10 -> 7 -> 5 -> 6). Six across
     the panel's inner width puts covers at 56-100px from 1024px up. It must stay
     in step with PER_ROW in KB/sync-vinyl.py, which decides how many covers are
     rendered at all: 6 x ROWS 5 = 30. Change both together or the last line
     comes out short. */
  grid-template-columns: repeat(6, minmax(0, 1fr));
  /* One uniform gutter on both axes. THE KNOB: at ~83px covers this is roughly a
     tenth of a sleeve, enough to read each one as a separate record while the
     block still holds together as a stack. Push past ~14px and it starts to read
     as a spaced grid again rather than a mass. */
  gap: clamp(6px, 0.7vw, 10px);

  /* The fade. A real alpha MASK, not a white gradient painted on top: the pixels
     genuinely go transparent, so this needs no knowledge of what is behind it and
     survives the dark theme, a coloured section, or anything else landing under
     the block later. If a browser ever ignores it the block simply renders solid,
     which is a graceful loss, unlike the grid bug above.

     --vinyl-fade-start is THE KNOB: covers are fully opaque to that point, then
     ramp to nothing at the bottom edge. 40% is the TOP OF ROW 3 in the 5-row
     desktop layout (Baha: the fade should reach the third row), so rows 1-2 stay
     solid and the dissolve runs across rows 3, 4 and 5. Raise it to keep more art
     readable, lower it for a heavier dissolve.

     A percentage, not a computed row height, on purpose: it is one number, it
     cannot break, and the effect is soft enough that being a few pixels off a row
     boundary is invisible. It now holds at EVERY width, because the wall is 6
     across everywhere: the covers shrink across breakpoints, the row count does
     not, so 40% stays the top of row 3.

     NB the faded covers are still in the accessibility tree and still announced,
     which is correct: they are content that is visually de-emphasised, not
     content that has been removed. */
  --vinyl-fade-start: 40%;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--vinyl-fade-start), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 var(--vinyl-fade-start), transparent 100%);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
/* The SQUARE lives on the cell, not the image, exactly like .toolstack__tile.
   A grid track is already the right width; aspect-ratio makes the cell as tall
   as it is wide, and the cover fills it. Doing this on the <img> instead makes
   the square depend on the image laying out correctly, which is precisely what
   failed above. */
.about-vinyl__item { min-width: 0; aspect-ratio: 1; }
.about-vinyl__cover {
  display: block;
  /* Fill the square cell and crop. Not every sleeve scan is square (a few mirror
     at 300x199), and the files keep their true ratio, so the crop is what keeps
     the wall a wall. The honest intrinsic size still ships in width/height, so
     the browser reserves the right box before decode: no CLS. */
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;   /* square, and it MUST be: at gap 0 any radius punches a white diamond where four sleeves meet */
  /* outline, not an inset box-shadow: on a replaced element the bitmap paints
     OVER an inset shadow, so it would never be seen. A negative offset pulls the
     ring inside the box, so it also costs no layout. Many sleeves have near-white
     borders that would otherwise dissolve into the page and eat the grid's edges. */
  outline: 1px solid rgba(14, 14, 14, .08);
  outline-offset: -1px;
  background: var(--surface-sunken);   /* the reserved box reads as a soft tile pre-decode, not a white hole */
}

/* "Visit my Discogs collection". Deliberately the SAME register as
   .about-hero__resume (gray text at --fs-lead, an icon, no button chrome), so
   the page has one link voice rather than a new button style per section. Two
   honest differences: the glyph is arrow-out-of-box because this LEAVES the
   site, and the hover nudges it up-and-right instead of down, so the motion
   says the same thing as the icon. */
.about-vinyl__link {
  /* Opened up from 24px on Baha's call. Note this reads LARGER than the number
     suggests: the stack's last row dissolves to nothing, so the block's real
     bottom edge sits well below its perceived one and that dead space adds to
     this margin. Retune together with --vinyl-fade-start, they compound. */
  margin-top: clamp(40px, 4vw, 64px);
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  font-size: var(--fs-lead);
  color: var(--fg-muted);
}
.about-vinyl__link:hover { color: var(--fg); }

/* Disc3, ported from Baha's React + Framer Motion component. The original spins
   a <motion.g> of two arcs 0 -> 90deg over 0.4s ease-in-out on hover, around the
   disc centre; that is exactly a CSS transition on a <g>, so no JS and no motion
   library ships. The outer ring and centre hole stay put, only the arcs turn.

   transform-box: view-box is the load-bearing line. It makes transform-origin
   resolve against the SVG's 24x24 viewBox, so "12px 12px" is the disc centre,
   matching the component's transformOrigin. It is already the CSS initial value
   for SVG elements, but stated explicitly because the alternative (fill-box)
   would spin the arcs around their OWN bounding box, well off-centre, and that
   failure is subtle enough to be worth naming.

   Reduced motion is handled globally in styles.css, which zeroes transition
   durations, so the rotation snaps instead of animating. Nothing needed here. */
.about-vinyl__link .disc { width: 1.15em; height: 1.15em; flex: none; }
.about-vinyl__link .disc__arcs {
  transform-box: view-box;
  transform-origin: 12px 12px;
  transition: transform .4s ease-in-out;
}
.about-vinyl__link:hover .disc__arcs,
.about-vinyl__link:focus-visible .disc__arcs { transform: rotate(90deg); }

/* ---- RIGHT HALF: the book collection ----
   Cols 8-12. It OPENS on grid line 8 because that is the line .about-spark__text
   opens on further up the page (Baha: "start the same line as The spark"), so the
   page's two right-hand blocks share a single edge. It runs to line 13, the
   content edge, like every other right-hand block here except the spark text
   itself, which stops at 12 to hold a reading measure. These panels are not text,
   so they take the full half.

   NB the block is named .about-books inside a section still classed
   .about-vinyl. The section is "Pieces of me" and the records were simply built
   first; renaming the section would churn every rule above for nothing. */
.about-books__title,
.about-books__shelves { grid-column: 8 / 13; }

.about-books__title {
  /* Peer of the records title: same role token, so the two halves open in one
     voice rather than at two different weights. */
  font-size: var(--fs-statement);
  line-height: var(--lh-display);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

/* Two boxes that must start and end on the same lines as the record panel
   opposite. The heading-to-shelf air is the parent's row-gap now, not a margin
   here, so both halves get the identical number by construction.

   align-content: space-between is what absorbs the difference in height between
   the two halves. The record panel is the taller of the two by 6-26px across
   desktop widths, so this column is the one that gets stretched, and the slack
   has to go somewhere. Sending it into the GAP keeps both boxes at their honest
   content height with even padding all round; the alternative, letting the boxes
   themselves stretch, would quietly give each one a fatter bottom padding than
   top, which reads as a mistake rather than as a decision.

   If the two halves ever swap and this column becomes the taller one, nothing
   breaks: there is simply no free space to distribute, and the record panel's
   align-content: start takes the stretch instead. */
.about-books__shelves {
  list-style: none; padding: 0; margin: 0;
  display: grid;
  align-content: space-between;
  gap: clamp(12px, 1.2vw, 20px);   /* the MINIMUM gap; it opens up by whatever the halves differ by */
}

.about-books__shelf {
  background: var(--surface-sunken);    /* the Cynoia gray, same fill as the tool-stack panel */
  border-radius: var(--radius-panel);   /* 16px, the house grouping-panel radius */
  padding: clamp(20px, 2.2vw, 32px);
  /* No min-height any more. The covers are the content, so the panel is sized by
     what is in it rather than by a placeholder number. */
}

/* Formatting lifted from .topbar__label ("Baha Jebali" in the meta header), per
   Baha. Deliberately NOT @extend-ed or aliased: the two live in different files
   and different registers, and tying them together would mean a future tweak to
   the masthead silently moved these labels. */
.about-books__label {
  font-size: var(--fs-body);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  margin-bottom: clamp(16px, 1.6vw, 24px);   /* label to shelf */
}

/* The shelf itself. A JUSTIFIED row: each cover's flex-grow is its own aspect
   ratio, with basis 0, so the widths distribute in proportion to the ratios and
   the row lands flush on both padding edges (Baha: "make the padding of the left
   the same as the right"). Free space goes into the covers, not into a ragged
   right margin.

   THE KNOB is now the gap, and it is the only number in the block. Everything
   else derives: cover width = (panel inner - gaps) x ratio / sum of ratios, and
   the height falls out of that. No --book-h, no per-breakpoint clamp, no
   overflow possible at any width, because basis 0 means the covers can only ever
   share out space that already exists.

   THE TRADEOFF, and it is a real one. Three things cannot all be true at once
   here: flush edges, one gap, one height. The three ratios on the reading list
   sum lower than the three on the currently-reading shelf, so filling both boxes
   with the same gap makes the reading-list covers about 10% taller. The
   alternative, uniform heights with the slack pushed into the gaps
   (justify-content: space-between), is worse: the geometry forces a fixed ~17px
   difference between the two shelves' gaps, which at these sizes reads as an 80%
   relative change and looks like a spacing mistake. A 10% height difference is
   the smaller lie. If you ever want the other trade, it is one declaration.

   Bottom-aligned because that is how books sit on a shelf. */
.about-books__row {
  list-style: none; padding: 0; margin: 0;
  display: flex;
  align-items: flex-end;
  gap: clamp(16px, 1.8vw, 32px);
}

.about-books__book {
  /* --ar is each cover's true width/height, set per book in the markup from the
     same numbers as its width/height attributes. The default here is not
     decoration: it guarantees the var() below always resolves, so `flex` can
     never fall back to its initial value the way the record wall's grid rule once
     did. The shorthand (not the three longhands) is deliberate for the same
     reason: if it ever did fail, the row degrades to natural widths rather than
     to zero-width items. */
  --ar: .68;
  flex: var(--ar) 1 0%;
  min-width: 0;
}

.about-books__cover {
  display: block;
  width: 100%;
  height: auto;   /* the width/height attributes carry the ratio, so the box is reserved before decode */
  /* Same hairline the record covers use. Load-bearing here, not decoration: two
     of these six covers are near-white, and without it they dissolve into the
     gray panel. Inset so it reads as the edge of the board, not a frame. */
  outline: 1px solid rgba(14, 14, 14, .08);
  outline-offset: -1px;
}

/* ---- FULL WIDTH: the film wall ----
   Row 3 of "Pieces of me", under the records/books pair and spanning all twelve
   canon columns. The records are a half-width panel because a sleeve is square
   and 30 of them fill a half nicely; a poster is 2:3, and 60 of them at a
   legible size only fit across the whole page. The width is derived from the
   content, not chosen for variety.

   Everything else is lifted from .about-vinyl__panel on purpose: same
   --surface-sunken fill, same --radius-panel, same padding, same
   stack-plus-link-in-one-container structure, same alpha-mask fade. Three
   panels in one section have to read as one family or the section reads as
   three unrelated experiments. If you retune one, retune all three. */
.about-vinyl__body > .about-films__title { grid-row: 3; }
.about-vinyl__body > .about-films__panel { grid-row: 4; }

.about-films__title {
  /* NOT 1 / -1. The PANEL is page-wide because the wall needs the width; the
     sentence does not. Run a --fs-statement line across the full 1440px content
     box and the measure goes past 100 characters, well beyond the 45-75 the
     line-length research supports. Eight columns lands it near 60ch and wraps it
     to two lines, which is also what makes the two <em> accents read as accents
     rather than as one long italic run. THE KNOB is the 9. */
  grid-column: 1 / 9;
  /* The body grid's uniform 48-80px row-gap is correct for heading-to-panel, and
     too tight for panel-to-next-heading: without this, row 2's panels and this
     heading sit the same distance apart as this heading and its own panel, and
     the film block reads as a continuation of the records rather than as its own
     movement. Margin, not row-gap, because only THIS gap is wrong. */
  margin-top: clamp(24px, 3vw, 48px);
  font-size: var(--fs-statement);
  line-height: var(--lh-display);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
  /* NB the serif italic on <em> is NOT set here. It comes from the shared accent
     list in styles.css, which .about-films__title em was added to. Setting it
     locally would fork the accent device into a second definition. */
}

/* Same grid-of-one as .about-vinyl__panel, for the same two reasons: it
   blockifies the link so `justify-self: start` governs it and no anonymous line
   box adds stray leading under the stack. */
.about-films__panel {
  grid-column: 1 / -1;
  display: grid;
  align-content: start;
  background: var(--surface-sunken);
  border-radius: var(--radius-panel);
  padding: clamp(20px, 2.2vw, 32px);
}
.about-films__link { justify-self: start; }

/* The wall: 30 posters butted into one stack, fading out at the bottom.

   THE INVARIANT, and it is the only thing here that can actually break: the
   rendered count (30) must be divisible by EVERY per-row value this file
   declares, or the last line comes out short and a ragged edge inside a
   container reads as an accident. 30 / 10 = 3, 30 / 6 = 5, 30 / 5 = 6. All
   clean. If you change the count, the legal per-row values change with it (the
   divisors of 30 in range are 10, 6, 5, 3); if you change a per-row value, it
   must stay a divisor. ROWS x PER_ROW in KB/sync-films.py is the same number and
   moves with it.

   HISTORY, because the ladder has been forced twice:
     12 x 5 = 60   ladder 12/10/6
     10 x 4 = 40   ladder 10/8/5   (6 does not divide 40)
     10 x 3 = 30   ladder 10/6/5   (8 does not divide 30)
   Do not reintroduce an 8 or a 12 here without changing LIMIT to match.

   Same minmax(0, 1fr) idiom the record wall documents at length: 1fr would floor
   each track at the poster's intrinsic width and blow the row past its column.
   Do not "simplify" it back to 1fr, and do not reintroduce a self-deriving
   auto-fill expression. */
.about-films__wall {
  list-style: none; padding: 0;
  margin: 0;   /* the panel owns the spacing */
  display: grid;
  grid-template-columns: repeat(10, minmax(0, 1fr));
  /* The same uniform gutter as the record wall, both axes. Matching the records
     is the point: at a glance the two stacks should look like the same object
     photographed at two sizes. */
  gap: clamp(6px, 0.7vw, 10px);

  /* The fade. A real alpha MASK, not a gradient painted on top, so the pixels
     genuinely go transparent and this survives the dark theme or anything else
     landing behind it. Identical machinery to --vinyl-fade-start.

     66%, and it is ONE value at every breakpoint despite the row count changing
     from 3 to 5 to 5. It is derived, not picked: THE RULE IS (2 / ROWS), because
     the intended read is TWO SOLID ROWS and then the mass trailing off. That is
     the same read the record wall opposite has, and it is what makes the two
     stacks look like one idea.

     Re-derive it whenever ROWS changes. It has moved with every count so far:
       5 rows (60 posters)   40%
       4 rows (40 posters)   50%
       3 rows (30 posters)   66%
     Get this wrong and the failure is quiet but real: 40% of three rows would
     start dissolving barely past row 1, on a wall that only HAS three, so a
     third of the curation would be a ghost.

     A percentage also cannot break the way a computed row height can. Raise it
     to keep more art readable, lower it for a heavier dissolve.

     The faded posters stay in the accessibility tree and are still announced.
     Correct: they are de-emphasised, not removed. */
  --films-fade-start: 66%;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--films-fade-start), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 var(--films-fade-start), transparent 100%);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
/* The RATIO lives on the cell, not the image, same as .about-vinyl__item. The
   track is already the right width; aspect-ratio makes the cell 3/2 as tall, and
   the poster fills it. 2:3 is the standard one-sheet, which is also what
   Letterboxd serves, so the crop below is almost always a no-op. */
.about-films__item { min-width: 0; aspect-ratio: 2 / 3; }
.about-films__poster {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;   /* the few non-2:3 posters crop rather than break the row */
  border-radius: 0;    /* square, to match the sleeves opposite; two radii in one section reads as an accident */
  /* outline, not inset box-shadow: on a replaced element the bitmap paints OVER
     an inset shadow. Negative offset keeps the ring inside the box, so it costs
     no layout. Plenty of posters are near-white at the edge and would otherwise
     dissolve into the panel. */
  outline: 1px solid rgba(14, 14, 14, .08);
  outline-offset: -1px;
  background: var(--surface-sunken);
}

/* "Visit my Letterboxd". Same register as .about-vinyl__link and
   .about-hero__resume: gray text at --fs-lead, an icon, no button chrome. One
   link voice on this page, not a new button style per section. */
.about-films__link {
  margin-top: clamp(40px, 4vw, 64px);   /* reads larger than it measures: the stack's last rows dissolve to nothing, so the block's real bottom edge sits below its perceived one. Retune with --films-fade-start, they compound. */
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  font-size: var(--fs-lead);
  color: var(--fg-muted);
}
.about-films__link:hover { color: var(--fg); }

/* Clapperboard, ported from Baha's React + Motion component to inline SVG plus
   two @keyframes. Same port as Disc3 above, and for the same reason: this site
   has no build step and ~40 lines of runtime JS, so shipping Motion to spin an
   icon is not a trade worth making.

   FAITHFUL TO THE ORIGINAL, both variants:
     VARIANTS       rotate [-10, -10, 0], 0.8s, times [0, .5, 1],    origin 4px 20px
     CLAP_VARIANTS  rotate [0, -10, 16, 0], 0.4s, times [0, .3, .6, 1], origin 3px 11px
   Motion's `times` map straight onto keyframe percentages, and its per-segment
   easeInOut is what CSS animation-timing-function already does between stops, so
   the curve matches rather than approximates. The board holds its -10deg tilt
   for the first half and then rights itself while the clapper snaps: that hold
   is the [-10, -10, ...] pair, not a mistake.

   transform-box: view-box is load-bearing. It resolves transform-origin against
   the 24x24 viewBox so "4px 20px" is the hinge at the board's bottom-left,
   matching the component. fill-box would spin each group around its own bounding
   box, which is subtly and unfixably wrong.

   overflow: visible on the <svg> because the rotation pushes strokes outside the
   viewBox; the original component sets it inline for the same reason.

   Reduced motion is handled globally in styles.css, which forces
   animation-duration to .001ms, so the icon snaps instead of playing. */
.about-films__link .clap { width: 1.15em; height: 1.15em; flex: none; overflow: visible; }
.about-films__link .clap__board,
.about-films__link .clap__top { transform-box: view-box; }
.about-films__link .clap__board { transform-origin: 4px 20px; }
.about-films__link .clap__top   { transform-origin: 3px 11px; }
.about-films__link:hover .clap__board,
.about-films__link:focus-visible .clap__board { animation: clap-board .8s ease-in-out; }
.about-films__link:hover .clap__top,
.about-films__link:focus-visible .clap__top   { animation: clap-top .4s ease-in-out; }

@keyframes clap-board {
  0%   { transform: rotate(-10deg); }
  50%  { transform: rotate(-10deg); }
  100% { transform: rotate(0deg); }
}
@keyframes clap-top {
  0%   { transform: rotate(0deg); }
  30%  { transform: rotate(-10deg); }
  60%  { transform: rotate(16deg); }
  100% { transform: rotate(0deg); }
}

/* ============================================================
   RESPONSIVE, 810px collapse (matches the home build).
   Every placement linearises to one column in DOM/reading order.
   ============================================================ */
@media (max-width: 810px) {
  .canon { grid-template-columns: minmax(0, 1fr); row-gap: var(--space-32); }
  .canon > * { grid-row: auto; }
  .about-hero__text,
  .about-hero__pa,
  .about-hero__pcol,
  .about-play__fig,
  .about-play__text,
  .about-spark__thumbs,
  .about-spark__text,
  .about-custom__media,
  .about-custom__text,
  .about-decades__text,
  .about-decades__media,
  .about-leap__text,
  .about-leap__media { grid-column: 1 / -1; }
  .about-play__text { align-self: auto; }   /* drop the desktop vertical centring once stacked */
  /* KEEP the desktop bleed rig here, do not reset it back inside the container.
     Baha, 2026-08-02: on the phone the bird must touch the ABSOLUTE right edge,
     no gutter. The desktop machinery already produces exactly that and needs no
     mobile variant, because below --container-max the centring term in
     --leap-bleed resolves to 0 and the bleed IS --pad-x: justify-self:end pins
     the box's right edge to the container edge, `width: 100% + bleed` pushes its
     left edge back onto the content edge, and `right: -bleed` slides the pair
     out so the right edge lands on the viewport. Net on a 390px phone: the
     picture runs from the text's left edge to the screen's right edge.
     .about-leap's overflow-x:clip absorbs the overhang, so no h-scrollbar.
     (The old reset lived here because dropping `right` while KEEPING the widened
     width does overflow. Drop neither or both, never one.) */
  .about-custom__text,
  .about-play__fig,
  .about-leap__text { margin-right: 0; }   /* drop the desktop gutter bleeds once full-width */
  /* The quote is a figcaption INSIDE the bled figure, so it inherits the whole
     overhang. Pad it back by exactly one bleed (the custom property inherits
     from the figure) or the italic runs off the screen edge. */
  .about-leap__quote { padding-right: var(--leap-bleed); max-width: none; }
  /* One column: no portrait A to align to, so give the resume link a real gap
     under the picture (margin-top:auto would collapse to 0) and read it left. */
  .about-hero__resume { margin-top: var(--space-16); align-self: flex-start; text-align: left; }

  /* ---- HERO + PLAY: the phone's own reading order ------------------------
     Baha, 2026-08-06. On the phone neither band is a text column beside a picture
     column any more, it is ONE narrative column, and the desktop DOM order
     (text-then-pictures in the hero, picture-then-text in the play band) reads
     wrong once stacked. Both bands want the same phone order, and it is the order
     a person actually reads in: the claim, the picture that proves it, then the
     argument.

       hero:  statement -> portrait -> resume link -> paragraphs
       play:  title     -> diagram  -> paragraphs

     display:contents is the whole mechanism. Each heading and its paragraphs live
     inside ONE wrapper with the media as a sibling, so no amount of `order` on
     the wrappers can slot a picture between a wrapper's own children. Dissolving
     the wrapper promotes its heading and its paragraphs to grid items in their
     own right, and then `order` interleaves them freely. No duplicated markup, no
     wrapper divs invented for the phone, and the desktop placement above is
     untouched: past 810px both wrappers are ordinary blocks again.

     THE ONE COST, so nobody hunts it later: an element with display:contents
     generates no box, so the [data-reveal] fade on these two wrappers does not
     run on the phone (opacity and transform need a box to apply to). The content
     simply renders, which is a graceful loss, not a blank screen. The hero is
     above the fold and fires on load anyway. If the play title should fade in on
     the phone too, the fix is one move in the markup: put data-reveal on the <h2>
     and the paragraphs instead of on .about-play__text.

     SPACING is explicit margins in these two bands, not the canon's uniform
     row-gap, because the rhythm here is deliberately uneven and one number would
     flatten it: the resume link is a CAPTION of the picture above it (16px, set
     on .about-hero__resume), the body text is a NEW MOVEMENT (48px). Zeroing the
     row-gap first means every gap in these bands is stated once, in one place,
     instead of being a token minus a token. */
  .about-hero .canon,
  .about-play .canon { row-gap: 0; }
  .about-hero__text,
  .about-play__text { display: contents; }   /* NB: this makes their grid-column above inert on the phone, by design */

  .about-hero__statement  { order: 1; }
  .about-hero__pa         { order: 2; margin-top: var(--space-32); }
  .about-hero__pcol       { order: 3; }   /* carries no margin: the resume link inside it owns the 16px */
  .about-hero .about-lede { order: 4; }

  .about-play__title      { order: 1; }
  .about-play__fig        { order: 2; margin-top: var(--space-32); }
  .about-play .about-lede { order: 3; }

  /* Heading-to-body opens from the desktop's 32px to 48. It has to: on the phone
     the picture now sits BETWEEN them, and at an equal 32 above and below, the
     picture would read as belonging to the paragraph under it rather than to the
     title over it. 32 up / 48 down is the proximity gradient that keeps it
     attached to its heading (Gestalt proximity, and the reason the resume link
     sits at 16). Both selectors still match: `order` repaints, it does not
     reorder the DOM. */
  .about-hero__statement + .about-lede,
  .about-play__title + .about-lede { margin-top: var(--space-48); }

  /* ---- Display headings on the phone: natural flow -----------------------
     Baha, 2026-08-06. Two changes, and deliberately only these two. Sizes are
     NOT touched: every heading keeps its role token.

     1. The hard breaks come out. They are DESKTOP COMPOSITION, not copy: on a
        328px column they cut a line at a third of the measure, the opposite of
        what the phone needs. They stay in the markup because they are still the
        right call at desktop widths. (.toolstack lives in styles.css but ships
        on this page only, so its phone override belongs here beside the other.)

     2. text-wrap: balance comes off with them, and this is the load-bearing
        half. Balance does not FILL lines, it EVENS them: it minimises the
        longest line in the block. With the <br>s gone that is why "judgement,"
        would not come up to line 1 even where it plainly fits (Baha caught this
        on his own screen, and he was right): two balanced lines of 343px beat a
        greedy 370 + 171 pair, so balance holds line 1 back at "It's about the".
        Balance is correct for a heading whose break is decorative. It is wrong
        for one that should run the full measure, which is the whole ask here.

     Both are phone-only. Past 810px the <br>s govern the composition and balance
     goes back to being the safety net it was written as. */
  .about-play__title br,
  .toolstack__title br { display: none; }
  /* `wrap`, not `normal`: it resets BOTH halves of the shorthand (mode: wrap,
     style: auto), i.e. plain greedy line filling. A browser too old to know the
     keyword ignores the declaration and simply keeps balancing, which is a
     graceful loss rather than a broken heading. */
  .about-hero__statement,
  .toolstack__title { text-wrap: wrap; }

  /* "Nevertheless, here is my tool stack" was sitting ON the panel. The grid
     that stacks the lead and the panel here carries no row-gap, so the only air
     between the subtitle and the gray box was the line box's own descender
     space: the measured gap is 0. A caption and a container should not touch.
     32px is this page's phone step for "a new object begins" (16 binds a caption
     to the picture above it, 32 opens a new element, 48 opens a new movement),
     so the subtitle stays tied to the title it belongs to while the panel now
     reads as its own thing. On the grid, not as a margin on the panel: the two
     children are what the gap is between, so the parent should own it. */
  .toolstack__grid { row-gap: var(--space-32); }

  /* ONE picture in the hero, not two (Baha, 2026-08-06). Side by side on the
     desktop the pair is a stagger; stacked on a phone it is the same beat played
     twice, and the second frame delays the resume link and the paragraphs by a
     full screen for nothing. The turntable carries the band alone. Hidden HERE
     ONLY: the desktop pair is unchanged. See the markup note on the figure for
     why that img is loading="lazy" — display:none does not stop an eager fetch. */
  .about-hero__pb { display: none; }
  /* Subgrid has no canon tracks to borrow once the page is one column,
     so give the thumbs their own 2-up and let the pictures auto-place. */
  .about-spark__thumbs { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .about-spark__thumb-a,
  .about-spark__thumb-b { grid-column: auto; }
  /* Undo the two-row pairing. One column means there is nothing to align across,
     so the four pieces simply stack in DOM order: statement, records, heading,
     shelves. grid-row and align-self both have to be released explicitly, or the
     headings stay pinned to row 1 and the two containers pile into row 2 on top
     of each other.

     THE SELECTOR IS THE FIX, do not simplify it back to `> *`. A media query adds
     no specificity, so `.about-vinyl__body > *` (0,1,0) LOST to the desktop
     `.about-vinyl__body > .about-vinyl__title` (0,2,0) no matter that it comes
     later in the file. grid-row: 1 and align-self: end survived the breakpoint,
     both headings printed on top of each other, and the book shelves printed on
     top of the record wall (Baha's phone, 2026-08-02). The :is() list matches the
     desktop rules' specificity exactly and, coming later, wins the cascade; the
     `> *` arm stays so anything added to this grid later still linearises. */
  .about-vinyl__body > :is(.about-vinyl__title, .about-vinyl__panel,
                           .about-books__title, .about-books__shelves,
                           .about-films__title, .about-films__panel),
  .about-vinyl__body > * {
    grid-column: 1 / -1;
    grid-row: auto;
    grid-column: 1 / -1;
    grid-row: auto;
    align-self: start;
  }
  .about-vinyl__body { row-gap: clamp(32px, 6vw, 48px); }
  /* PANEL-TO-NEXT-HEADING, and stacked there are now two of those boundaries in
     this band: records panel -> "2026 reading list", and the shelves -> "Films
     that heavily influenced ...". The film heading already buys itself extra air
     on top of the row-gap (see .about-films__title, which needs it for the same
     reason: heading-to-its-own-panel and panel-to-next-heading are different
     joints and must not measure the same). The books heading never did, so on the
     phone one boundary read tight and the identical one below it read open
     (Baha, 2026-08-06). The SAME EXPRESSION, deliberately, not a matched number:
     retune one and the other follows instead of silently drifting. */
  .about-books__title { margin-top: clamp(24px, 3vw, 48px); }
  /* No shelf rule needed here. The justified row derives its own size from
     whatever width the panel has, so the jump from 5 columns to the full
     container at this breakpoint needs no re-tuning: the covers simply get
     bigger, which is what a full-width panel should do. */
  .about-body { max-width: none; }
}

/* NO breakpoint rule for the wall any more, and that is the point of the panel.

   The old 811-1200 rule existed because the stack was pinned to cols 1-4 (one
   Vice City picture) inside a 6-column half, which at those widths put covers
   under 40px, so the wall had to break its own alignment and take the full six
   columns. The panel now spans all six columns at EVERY width, so the mismatch it
   was patching no longer exists.

   The layout is therefore 6x5 everywhere: the covers shrink with the viewport,
   the row count never changes, and the fade's 40% keeps meaning "top of row 3".

   THE SOFT SPOT, worth knowing: covers are 56-100px from 1024px up and ~43px at
   811px, the narrowest the desktop split ever gets. That is small for a sleeve.
   It is a fade-masked decorative mass rather than something anyone reads, so it
   holds, but if Baha wants it fixed the lever is a band rule dropping to 5 across
   below ~1000px, at the cost of a 6-row stack there. */

/* No phone rule needed either: once the canon collapses the lead spans the full
   container, so six across lands at ~43px on a 390px phone and ~102px by 810px. */

/* THE FILM WALL DOES need band rules, and here is why it differs from the record
   wall directly above.

   The record wall holds 6 across at every width on purpose, so its row count is
   constant and its 40% always means "top of row 3". It gets away with that
   because a sleeve is square: at the narrowest desktop split a cover is ~43px,
   small but still a legible coloured square.

   A poster is 2:3. Ten across a full-width panel is ~128px on a large screen,
   which is a readable one-sheet, and ~55px at 810px, which is barely a poster.
   Holding 10 everywhere would trade the whole point of the block for one fewer
   rule. So the count steps down and the row count rises to compensate.

   NOTE THE MISSING 811-1200 BAND, which earlier versions had. At 30 posters the
   legal counts are 10, 6, 5 and 3, and the only one that suits that band is the
   10 already in force, so the band has nothing left to say. Ten across at 1200px
   is ~87px, still a legible poster. The step-downs now hang off the page's own
   810 and 560 breakpoints rather than off a number invented for this block.

   THE PHONE BAND BREAKS THE DIVISOR RULE ON PURPOSE, and this is the one place
   in the block where reading the CSS without this note would mislead. 4 does not
   divide 30. Baha asked for 4 x 5 on the phone (2026-08-04) so the block stops
   being a long scroll, and 4 x 5 is 20, not 30. So the phone does not reflow the
   thirty, it renders the FIRST TWENTY and hides the rest:

     30 items, 4 across, nothing hidden  ->  7.5 rows, a half-empty last line
     20 items, 4 across, 10 hidden       ->  5 clean rows

   The rule therefore generalises rather than breaks: the RENDERED count must
   divide by the column count. 30/10, 30/6, 20/4. sync-films.py still ships all
   thirty and needs no change, because this is a display concern, not a data one.

   The cost is that those ten posters leave the accessibility tree on phones, so
   a screen reader there hears twenty films and elsewhere thirty. Accepted: the
   wall is decorative, the twenty it announces are the front of the curation, and
   the Letterboxd link underneath is what actually carries the full set. This is
   different from the fade above, which de-emphasises without removing, and the
   difference is deliberate: hidden from sight and hidden from AT should agree. */
@media (max-width: 810px) {
  .about-films__wall { grid-template-columns: repeat(6, minmax(0, 1fr)); }   /* 6 x 5 = 30 */
}
@media (max-width: 560px) {
  .about-films__wall { grid-template-columns: repeat(4, minmax(0, 1fr)); }   /* 4 x 5 = 20 shown */
  /* n+21, i.e. everything past the twentieth. Pair this number with the column
     count above: it must always leave a multiple of that count standing. */
  .about-films__item:nth-child(n+21) { display: none; }
}
