/* ==========================================================================
   ABOUT — MAGAZINE FEATURE
   Aug 11 2026. Owns the About page's magazine treatment: the scroll-revealed
   masthead, the parallax opener, the floated figures the text wraps around,
   and the section furniture.

   --about-build: 5

   WHY A FILE AND NOT AN INLINE <style> THIS TIME. The previous About kept its
   furniture inline for a good reason (single-page classes do not belong in
   components.css, which six pages load). This version carries a JS partner —
   js/about-feature.js — and a stylesheet and its script should be findable in
   the same two places every other feature on this site is. Everything here is
   still `ks-` prefixed and still used on exactly one page; if a second page
   ever needs one of these, extract it then.

   THE PAGE IS TRANSPARENT BY DESIGN. Not one rule below sets a background.
   about.html is the only page whose spine column actually renders, and its
   polarity is deliberately inverted (HANDOFF 19); a panel behind the article
   would hide the thing this page exists to show. Contrast comes from the
   scrims ON the images, never from a surface under the text.
   ========================================================================== */

:root { --about-build: 5; }

/* Anton is the one face added outside the brand set — the compressed weight a
   rap magazine sets its cover in. It runs the masthead, the section titles and
   the pull lines ONLY. Body stays Source Serif 4, labels stay IBM Plex Mono,
   the standfirst stays Big Shoulders. Loaded in about.html's <head> alongside
   the existing families. */
:root { --font-mag: 'Anton', Impact, 'Arial Narrow', sans-serif; }

/* ---- Grain ---------------------------------------------------------------
   Fixed, not scrolled, so it reads as a property of the paper rather than of
   the content. mix-blend-mode: overlay against a near-black page is a very
   small effect — 0.07 is already at the top of what is visible without the
   text starting to look dirty. */
.ks-grain {
  position: fixed; inset: 0; z-index: 60; pointer-events: none;
  opacity: 0.07; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---- The masthead --------------------------------------------------------
   A 200vh TRACK with a sticky 100vh stage inside it. The extra 100vh is the
   scroll distance the reveal is measured against — the animation is tied to
   how far the reader has scrolled, not to a timer, so it lands identically on
   a 900px laptop and a 1440px monitor. js/about-feature.js does the measuring;
   this file only sets the geometry it measures.

   Do not reduce the track below ~180vh. Under that the whole reveal completes
   in the first flick of a trackpad and reads as a jump cut. */
.ks-hero { position: relative; height: 200vh; }
.ks-hero__stage {
  position: sticky; top: 0;
  /* NO min-height, DELIBERATELY. It was 600px and it put the headline block and
     the cue below the fold on any viewport shorter than that — measured at 540px
     tall, the cue's rect sat at y=570. The stage is exactly one viewport and the
     type block sizes itself; if the type needs a floor, give it to the type. */
  height: 100vh;
  overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
}

/* THE BLEED IS ASYMMETRIC ON PURPOSE — -4% top, -24% bottom. The JS derives
   its maximum travel from the BOTTOM bleed (the only edge upward motion can
   expose), so this is the one knob for parallax strength. Build 1 hardcoded
   0.4 * vh in the JS instead, ~3x what the then-symmetric -14% provided, and
   the photo's bottom edge rose into the stage mid-reveal — "SPINES" sat half
   on sky and the scrim (a child of this element) left with the photo. Owner
   caught it Aug 12 2026.

   WHY 4/24 AND NOT 14/14: object-fit cover means total bleed sets the side
   crop (28% total either way — identical), but only the bottom share is
   travel. Measured at 1440x900: 216px of drift instead of 126px, byte-identical
   end frame, and the rest frame gains cap headroom (visible image band at rest
   is rows 3–81% instead of 11–89%; the caps sit at ~12.5% and clear it either
   way, with far more margin now). Going past 28% total (e.g. 8/32) buys more
   drift but tightens the side crop toward the figures — measured 64% of image
   width visible vs 70% — so the total stays at 28%. */
.ks-hero__media { position: absolute; left: 0; right: 0; top: -4%; bottom: -24%; will-change: transform; }
.ks-hero__media img { width: 100%; height: 100%; object-fit: cover; object-position: center 46%;
  filter: grayscale(0.42) contrast(1.16) brightness(0.58); }

/* Two layers over the photograph, in this order and for different jobs:
   the halftone is texture, the gradient is legibility. Swapping them puts the
   dot screen on top of the scrim, where it stops reading as print. */
.ks-halftone {
  position: absolute; inset: 0; pointer-events: none;
  mix-blend-mode: multiply; opacity: 0.45;
  background-image: radial-gradient(circle, rgba(0,0,0,0.9) 0.9px, transparent 1.1px);
  background-size: 4px 4px;
}
.ks-hero__scrim {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(3,4,15,0.90) 0%, rgba(3,4,15,0.12) 30%, rgba(3,4,15,0.50) 62%,
    rgba(3,4,15,0.93) 90%, #03040F 100%);
}
/* The top stop is for the fixed nav, which is opaque black once scrolled but
   transparent at the top of this page — the headline sits under it otherwise. */

.ks-hero__inner {
  position: relative; z-index: 2;
  width: 100%; max-width: var(--container-max); margin-inline: auto;
  padding: 0 var(--container-pad) clamp(44px, 8vh, 96px);
  display: flex; flex-direction: column; gap: clamp(14px, 2.2vh, 26px);
}
.ks-hero__eyebrow { display: flex; align-items: center; gap: var(--space-4); opacity: 0; transform: translateY(14px); }
.ks-hero__rule { display: block; width: clamp(28px, 6vw, 72px); height: 1px; background: var(--color-crimson-lit); }

/* THE HEADLINE. Each word sits in its own overflow:hidden mask so the letters
   can wipe up out of a crop — printed onto the photograph rather than faded in
   over it. The h1 keeps its two lines as separate elements because they are
   revealed on overlapping but different scroll segments. */
.ks-mast { margin: 0; display: flex; flex-direction: column; }
.ks-mast__mask { display: block; overflow: hidden; }
.ks-mast__word {
  display: block;
  font-family: var(--font-mag);
  font-weight: 400;             /* Anton ships one weight; 700 synthesises and smears */
  text-transform: uppercase;
  font-size: clamp(3.4rem, 13.5vw, 11.5rem);
  line-height: 0.92;
  letter-spacing: -0.005em;
  color: var(--text-primary);
  /* JS writes clip-path + transform per frame. The starting state is set here
     so the words are hidden before the first frame runs — without it they flash
     at full size on load, which is the whole animation given away for free. */
  clip-path: inset(108% 0% 0% 0%);
}
/* No-JS and reduced-motion both need the headline VISIBLE, not animated. */
.no-js .ks-mast__word, .ks-hero.is-static .ks-mast__word { clip-path: none; transform: none; }
.ks-hero.is-static .ks-hero__eyebrow, .ks-hero.is-static .ks-standfirst { opacity: 1; transform: none; }

.ks-standfirst {
  border-top: 1px solid var(--border-subtle);
  padding-top: clamp(14px, 2.2vh, 24px);
  max-width: 1000px;
  opacity: 0; transform: translateY(14px);
}
.ks-standfirst p {
  margin: 0; max-width: 62ch;
  font-family: var(--font-display);
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em;
  font-size: clamp(1.05rem, 1.8vw, 1.6rem); line-height: 1.25;
  color: var(--text-secondary);
}
.ks-hero__cue {
  position: absolute; left: 50%; bottom: clamp(14px, 2.4vh, 26px); transform: translateX(-50%);
  z-index: 2;
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--color-gray-400);
}

/* ---- Section furniture --------------------------------------------------- */
.ks-feature { max-width: var(--container-max); margin-inline: auto; padding-inline: var(--container-pad); }
.ks-section { margin-top: clamp(60px, 9vh, 120px); }

.ks-head {
  display: flex; align-items: baseline; gap: 18px;
  border-bottom: 1px solid var(--border-subtle);
  padding-bottom: var(--space-3);
  margin-bottom: clamp(24px, 4vh, 40px);
}
.ks-head__num { font-family: var(--font-mono); font-size: var(--fs-mono); letter-spacing: 0.14em; color: var(--color-crimson-lit); }
.ks-head h2 {
  margin: 0; font-family: var(--font-mag); font-weight: 400;
  text-transform: uppercase; letter-spacing: 0;
  font-size: clamp(1.9rem, 4.6vw, 3.6rem); line-height: 1;
  color: var(--text-primary);
}

.ks-copy { max-width: none; margin: 0 0 1.35em; }
.ks-copy:last-child { margin-bottom: 0; }
.ks-copy strong { color: var(--text-primary); font-weight: 600; }
.ks-body { font-size: 1.0625rem; line-height: 1.78; color: var(--text-secondary); max-width: 72ch; }
/* The wider measure is ONLY for sections carrying a floated figure. 92ch minus
   a ~400px picture leaves roughly a 50ch column beside it — a real magazine
   column. The tail lines that clear the image run the full 92ch, which is wide,
   but only ever for the last line or two of a paragraph. A float with a 72ch
   cap beside it leaves a hole instead of a wrap; that was measured and fixed
   on the previous version of this page and the lesson carries over. */
.ks-body--flow { max-width: 92ch; }

.ks-lede { margin: 0; max-width: 70ch; font-size: clamp(1.2rem, 1.9vw, 1.6rem); line-height: 1.45; color: var(--text-primary); }

.ks-pull {
  margin: 0 0 1.35em; max-width: none;
  padding-left: var(--space-6);
  border-left: 2px solid rgba(214, 213, 208, 0.34);
  font-family: var(--font-mag); font-weight: 400; text-transform: uppercase;
  font-size: clamp(1.6rem, 3.4vw, 2.5rem); line-height: 1.08;
  color: var(--text-primary);
}
.ks-shout {
  margin: 0; max-width: none;
  font-family: var(--font-mag); font-weight: 400; text-transform: uppercase;
  font-size: clamp(1.5rem, 2.8vw, 2.2rem); line-height: 1.08;
  color: var(--text-primary);
}

.ks-signoff { margin-top: clamp(40px, 6vh, 72px); border-top: 1px solid var(--border-subtle); padding-top: clamp(28px, 4vh, 44px); }
.ks-signoff__mark {
  margin: 0; max-width: none;
  font-family: var(--font-mag); font-weight: 400; text-transform: uppercase;
  font-size: clamp(2.2rem, 7vw, 5.5rem); line-height: 0.94; color: var(--text-primary);
}
.ks-signoff__sub {
  margin: var(--space-3) 0 0; max-width: none;
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--color-crimson-lit);
}

/* ---- Figures ------------------------------------------------------------- */
/* THERE ARE NO CAPTIONS. Owner's call 2026-08-11; .ks-figcap and .ks-bleed__cap
   are gone along with all four of them. They were also the page's worst
   legibility failure: 11px --color-gray-400, which tops out at 3.94:1 on flat
   #03040F and so could not reach AA anywhere on this site regardless of
   backdrop. Measured over the lit nebula the Graveyard Shift caption ran
   1.87:1 with 100% of its glyph pixels under the threshold.
   The images carry real alt text; the captions were duplicating what the
   surrounding prose already says. If captions ever come back they need a
   colour at or above --color-gray-300 and the two-pass shadow. */

/* THE FLOATED CUT — the move that makes this read as a spread and not a blog
   post: the paragraphs WRAP the picture. Float, not grid; shape-outside pins
   the wrap to the margin box so the caption does not push the text away.
   The sources are square-ish and the figures stand full-length in them, so
   nothing here crops vertically — the images run at their own ratio and the
   column bends around them. */
.ks-cut { margin: 0.4em 0 1.8em; }
.ks-cut img, .ks-cut video { width: 100%; display: block; border: 1px solid rgba(214, 213, 208, 0.14); }

/* BARE — for a cut whose source is TRANSPARENT rather than a photograph.
   The border above is right for the messenger photos: it gives a rectangle of
   footage a defined edge against the page. On line art with an alpha channel
   there is no rectangle to edge, so the border draws a frame around mostly
   empty space and the art appears to sit in a box it does not fill.
   Aug 20 2026, added for the infinity spine in section 02. */
.ks-cut--bare img { border: none; }
@media (min-width: 881px) {
  .ks-cut--right { float: right; width: min(40%, 400px); margin: 0.4em 0 1.8em var(--space-12); shape-outside: margin-box; }
  .ks-cut--left  { float: left;  width: min(38%, 380px); margin: 0.4em var(--space-12) 1.8em 0; shape-outside: margin-box; }
  .ks-cut--wide  { float: right; width: min(46%, 480px); margin: 0.4em 0 1.8em var(--space-12); shape-outside: margin-box; }
}
/* Every float needs releasing or the next heading rides up beside it. */
.ks-clear { clear: both; }

/* FULL-BLEED. `margin-inline: calc(50% - 50vw)` is the standard escape from a
   centred container. 100vw INCLUDES THE SCROLLBAR on desktop Chrome, which is
   how this trick normally introduces a horizontal scrollbar of its own — the
   page is checked for overflow after every change here, and <body> keeps
   overflow-x: CLIP as the belt to this braces.

   CLIP, NOT HIDDEN, AND THIS ONE IS NOT NEGOTIABLE. `overflow-x: hidden`
   computes overflow-y to `auto`, which makes that element a scroll container —
   and a scroll container is the nearest scrolling ancestor for anything
   `position: sticky` inside it. With `hidden` on body, .ks-hero__stage sticks to
   a box that never scrolls, i.e. it does not stick at all: measured, the stage
   scrolled away like a normal block, the reveal finished with the headline
   already half above the viewport, and the back half of the 200vh track rendered
   as an empty black screen. `clip` does the same visual job and creates no
   scroll container, so the sticky survives. */
.ks-bleed { position: relative; margin-inline: calc(50% - 50vw); width: 100vw; max-width: 100vw; overflow: hidden; }
.ks-bleed img { width: 100%; height: auto; display: block; }
.ks-bleed--rule { border-block: 1px solid rgba(214, 213, 208, 0.14); }
.ks-bleed--faded::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, #03040F 0%, rgba(3,4,15,0) 22%, rgba(3,4,15,0) 78%, #03040F 100%);
}

/* ---- Two-column setting -------------------------------------------------- */
/* Only on sections with NO figure. Floats inside a multicol container are
   scoped to their column and break in ways no browser agrees on; the sections
   that carry a picture stay single-column and float, the ones that carry only
   prose go two-up. That alternation is most of what gives the page its rhythm. */
.ks-cols { columns: 2; column-gap: clamp(28px, 4vw, 56px); column-rule: 1px solid var(--color-gray-700); }
.ks-cols p { break-inside: avoid-column; }

/* ---- Scroll reveal ------------------------------------------------------- */
/* The transition lives here; js/about-feature.js only adds .is-in. Elements
   start hidden ONLY when the script is present to un-hide them — .no-js keeps
   everything visible, because a reveal that never fires is a blank page. */
.ks-r { opacity: 0; transform: translateY(22px);
  transition: opacity var(--motion-slow) var(--ease-standard), transform var(--motion-slow) var(--ease-standard); }
.ks-r.is-in { opacity: 1; transform: none; }
.no-js .ks-r { opacity: 1; transform: none; }

/* ---- Legibility over the sky ---------------------------------------------
   MEASURED 2026-08-11 at the brightest point the sky can reach (t = 11972ms:
   all four star bands on their 46% peak, the cloud at 99.5% of its own).
   This page sets NO background by design, so the prose is read against the
   nebula. Where a paragraph crosses the lit arm, 10-29% of its glyph pixels
   measured under 4.5:1 and whole clauses washed out -- worst in sections 08,
   09 and 10. The mean was never the problem: bone at 17px means 6.5-9.5:1.

   Same two-pass pair spine-bg.css uses three times for small text over the lit
   column. The 1px pass gives the glyph a hard edge, the 3px pass kills the
   halo, and rgba(3,4,15) rather than #000 so it can never paint below the page.
   Measured effect at the worst sites: worst-5% 2.31 -> 6.68 (sec 08 @1440),
   1.93 -> 6.45 (sec 09 @1920), 1.25 -> 15.23 (.ks-shout @1920).

   TO MEASURE A SHADOW, DO NOT COMPARE THE GLYPH CORE TO THE BARE BACKDROP.
   A shadow changes neither of those pixels, so it will read as doing nothing.
   Capture the backdrop with the shadow painted and the fill transparent -- a
   shadow still paints from a transparent glyph's outline.

   The scrim alternative does not apply: the scrims live ON the images
   (.ks-hero__scrim, .ks-bleed--faded::after) and none of the failing prose
   sits over an image. Every failure is over bare sky, which by this file's own
   design has no surface behind it. */
.ks-lede, .ks-copy, .ks-shout, .ks-pull,
.ks-head h2, .ks-head__num, .ks-signoff__mark, .ks-signoff__sub,
.ks-standfirst p, .ks-hero__cue, .ks-hero__eyebrow .label {
  text-shadow: 0 0 3px rgba(3, 4, 15, 0.98), 0 1px 2px rgba(3, 4, 15, 0.92);
}

/* A SHADOW CANNOT FIX A COLOUR THAT FAILS ON BLACK, and --color-gray-400
   (#6B6B6B) tops out at 3.94:1 on flat #03040F -- it cannot reach AA anywhere
   on this site regardless of backdrop. The captions that shared this problem
   were deleted outright (see "Figures" above); .ks-hero__cue is what is left.
   Same move css/spine-bg.css already makes for .label. */
.ks-hero__cue { color: var(--text-secondary); }

/* STILL FAILING, DELIBERATELY, PENDING AN OWNER CALL ON THE BRAND.
   --color-crimson-lit (#8A1414) tops out at ~2.2:1 on flat #03040F, so
   .ks-head__num (the 01-10 numerals) and .ks-signoff__sub ("Decode the
   transmission.") fail before the sky is involved at all -- measured
   1.09-1.80:1. The shadow above helps them and does not save them.
   Four candidates were measured against the shadowed backdrop: #D9433A
   (3.2-3.7), #E8574B (3.9-4.5), #F0655A (4.5-5.3, no margin) and bone
   (9.4-11.2). NOTHING STILL RECOGNISABLY CRIMSON CLEARS AA. The fix is
   `color: var(--text-secondary)` on both, and it costs the accent.
   .ks-head__num has a defensible decorative exemption -- it duplicates no
   information and sits beside its own heading. .ks-signoff__sub is real
   content and has none. */

/* ---- Narrow --------------------------------------------------------------
   880px is where a floated 400px figure leaves under 40ch beside it, which is
   below the point prose stops being readable — so the floats drop out entirely
   rather than being shrunk. The columns collapse at the same width for the
   same reason. This block sits ABOVE nothing else in this file, but note the
   site convention: any page-scoped block in the shared stylesheets goes above
   the ≤600px block. That does not apply here (this file has no page scoping),
   but do not import that habit backwards into spine-bg.css. */
@media (max-width: 880px) {
  .ks-cols { columns: 1; }
  .ks-cut { float: none !important; width: 100% !important; max-width: 520px; margin: 0 0 var(--space-8) 0 !important; }
  .ks-body--flow { max-width: 72ch; }
}

@media (prefers-reduced-motion: reduce) {
  .ks-r { opacity: 1 !important; transform: none !important; transition: none !important; }
  .ks-mast__word { clip-path: none !important; transform: none !important; }
  .ks-hero__eyebrow, .ks-standfirst { opacity: 1 !important; transform: none !important; }
  /* The 200vh track is left alone deliberately: collapsing it would relayout
     the page for these users and the sticky stage is not itself motion. */
}
