/* ============================================================
   LAYOUT: the counted canvas. Page shell, the content
   widths every section chooses from, header, hero, footer,
   mobile sheet. Section internals live in sections.css.
   ============================================================ */

/* ── The canvas ────────────────────────────────────────────────
   One grid, two named widths. A section does not set its own
   container; it takes a width off the canvas, and the rhythm of the
   page comes from what each section does inside that width rather
   than from stacked cards.

     bleed   edge to edge, for the one inverted surface
     wrap    the 1240 container, every section's home width

   Every section shares one left edge. Exactly one, About, steps off
   it, which is what makes that step read as a decision.
   Gutters absorb the safe-area inset so nothing sits under a notch. */
:root {
  --pad-l: max(var(--grid-pad), env(safe-area-inset-left));
  --pad-r: max(var(--grid-pad), env(safe-area-inset-right));
}

.canvas {
  display: grid;
  grid-template-columns:
    [bleed-start] minmax(var(--pad-l), 1fr)
    [wrap-start] minmax(0, var(--grid-max))
    [wrap-end] minmax(var(--pad-r), 1fr)
    [bleed-end];
}
.canvas > * { grid-column: wrap; }
.canvas > .u-bleed { grid-column: bleed; }

.section { padding-block: var(--space-section-y); }
/* The hero already supplies the space above the first section, which is
   now About rather than Work. */
.section--about { padding-block-start: var(--space-block-y); }

/* `content-visibility: auto` used to sit on the four sections below the
   fold. It is gone on purpose: a skipped subtree reports zero-size boxes
   to IntersectionObserver, so the markers and marks inside those four
   sections never received their draw trigger. The page is 514 KB and
   eight sections deep, so the rendering it saved was not worth a
   feature that silently did not run. */

/* ── Section head ──────────────────────────────────────────────
   Heading, and where the section earns one, a single deck line.
   No printed section number and no label above the heading: the
   counted marker in the gutter carries the order instead. */
.section__head {
  position: relative;
  margin-block-end: var(--space-block-y);
}

.section__title {
  font-size: var(--t-d3);
  line-height: var(--lh-d3);
  letter-spacing: var(--ls-d3);
}
/* Chapter openers carry more weight than ordinary section titles. */
.section__title--chapter {
  font-size: var(--t-d2);
  line-height: var(--lh-d2);
  letter-spacing: var(--ls-d2);
}

.section__lead {
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  color: var(--color-text-mute);
  max-width: 42ch;
  margin-block-start: var(--p-space-4);
}

/* ── The counted marker ────────────────────────────────────────
   A section's place in the order, written in the module itself:
   one stitch per section, needle first.

   Counted in TWO columns, not one. Counted embroidery has never
   counted in a line, it counts in a grid, and a single file of
   stitches turned into a 74px hairline by the seventh section.
   Two columns cap the run at four rows however far the page grows:

     1 ▪        3 ▪▪       5 ▪▪      7 ▪▪
                  ▪          ▪▪        ▪▪
                             ▪         ▪▪
                                       ▪

   Purely decorative: it is hidden wherever the gutter is too
   narrow to hold it, and nothing depends on it being read. */
.mark {
  position: absolute;
  inset-block-start: 0.35em;
  inset-inline-start: calc(-1 * var(--mark-inset));
  display: none;
  grid-template-columns: repeat(2, var(--mark-unit));
  gap: var(--mark-gap);
}
/* The stitch starts small and pale, not absent. Nothing in the world
   appears out of nothing, and scale(0) is the one shape a stitch can
   never have. 0.4 keeps the point of the mark, which is that the
   stitch is being pulled tight, without claiming it was not there. */
.mark i {
  inline-size: var(--mark-unit);
  block-size: var(--mark-unit);
  background: var(--color-stitch);
  transform: scale(0.4);
  opacity: 0;
  transition: transform var(--p-dur-2) var(--p-ease-out),
              opacity var(--p-dur-2) var(--p-ease-out);
}
/* The needle: the first stitch of the run is the one in thread. */
.mark i:first-child { background: var(--color-stitch-active); }

/* Stitched in one at a time, in the order they would be sewn. */
.mark[data-drawn="true"] i { transform: scale(1); opacity: 1; }
.mark[data-drawn="true"] i:nth-child(2) { transition-delay: 50ms; }
.mark[data-drawn="true"] i:nth-child(3) { transition-delay: 100ms; }
.mark[data-drawn="true"] i:nth-child(4) { transition-delay: 150ms; }
.mark[data-drawn="true"] i:nth-child(5) { transition-delay: 200ms; }
.mark[data-drawn="true"] i:nth-child(6) { transition-delay: 250ms; }
.mark[data-drawn="true"] i:nth-child(7) { transition-delay: 300ms; }

@media (min-width: 1280px) {
  .mark { display: grid; }
}

/* ── Header ────────────────────────────────────────────────────
   An ordinary band across the top: the hero is a separate block that
   begins below it, not a canvas the header floats on. No rule at the
   bottom either: the hero starts flush against the band, so its own
   top edge is the line, and a hairline as well would be two edges
   drawing the same boundary.

   Below the hero there is no canvas edge left to borrow, so the band
   earns its own boundary without a rule: it lifts onto
   --nav-bg-raised, a step lighter than the page in both themes, with
   --shadow-nav under it. The shadow is the light theme's half of that
   job; on the dark page it is nearly invisible and the tone step
   carries the boundary alone, which is expected. Both arrive on the
   scroll timeline; see .site-header::after just below.

   Fixed rather than sticky, with the space it covers reserved on
   <main>. A sticky band that shrinks takes 16px out of the flow and
   shoves the whole page up with it; a fixed one shrinks over content
   that has already scrolled past, so nothing moves.

   TO TURN THE SHRINK OFF: delete the @supports block further down that
   carries the header-compact animation. The band then stays at its full
   height and nothing else on the page changes. */
.site-header {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: var(--p-z-nav);
  background: var(--nav-bg);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
/* The raised state of the band, off at rest. On the first screen the
   band is the page and nothing separates them: the hero canvas draws
   its own hard top edge just below, and a second boundary over the
   same line is exactly what §30 removed. Both halves of the raised
   state, the lighter surface and the shadow under it, live on this one
   layer and fade in together over the first 160px of scroll, on the
   same timeline as the compact animation below.

   Faded with opacity rather than animating background-color and
   box-shadow directly: a keyframe whose value comes from a custom
   property is dropped, and the band finished the range with no shadow
   at all. Opacity always interpolates.

   It paints above the header's own background and below the header's
   content, which is what makes the crossfade work: negative z-index
   descendants come after the stacking context's own background but
   before its in-flow content.

   `pointer-events: none` and `z-index: -1` are not optional. This is a
   full-size absolute box over the whole band, which is exactly the
   shape that swallowed every click in the header once before. */
.site-header::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--nav-bg-raised);
  box-shadow: var(--shadow-nav);
  opacity: 0;
}
/* The reserved space reads the ROOT height, never the compact one, which
   is what keeps the page still while the band moves. */
#main { padding-block-start: var(--nav-h); }

.site-header__inner {
  /* Not the 1240 container: the canvas edge. The wordmark begins exactly
     where the hero begins and the CTA ends exactly where it ends, so the
     band and the canvas read as one object seen from the same edges. */
  grid-column: bleed;
  margin-inline: var(--canvas-inset);
  position: relative;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--p-space-5);
  transition: height var(--p-dur-2) var(--p-ease-out);
}

/* Compact state, driven by the document's own scroll timeline rather
   than by an observer. It is a continuous question, not a threshold
   one, and the band tightens gradually over the first 160px instead of
   snapping at a line. No JavaScript is involved, so there is no
   callback that can fail to arrive and leave the two out of step.

   Animating height is normally worth avoiding, but the header is fixed
   and therefore out of flow: its height changes lay out nothing except
   itself. Where scroll timelines are unsupported the band simply stays
   at full height, which is the layout the page is designed around. */
@supports (animation-timeline: scroll()) {
  @keyframes header-compact {
    from { height: var(--nav-h); }
    to   { height: var(--nav-h-compact); }
  }
  @keyframes wordmark-compact {
    from { font-size: var(--t-h5); }
    to   { font-size: var(--t-body-sm); }
  }
  /* The shadow arrives with the tightening, not before it: at rest the
     hero canvas is the boundary, once the page moves the band has to
     carry its own. Same timeline, same range, so it reads as one
     gesture rather than two things happening near each other. */
  @keyframes header-lift {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  .site-header::after,
  .site-header__inner,
  .site-header .wordmark {
    animation: linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 160px;
  }
  .site-header::after { animation-name: header-lift; }
  .site-header__inner { animation-name: header-compact; }
  .site-header .wordmark { animation-name: wordmark-compact; }
}

/* Reduced motion keeps the band at one size rather than resizing it
   under the reader; the global rule only shortens durations, which a
   scroll-driven animation does not have. */
@media (prefers-reduced-motion: reduce) {
  .site-header::after,
  .site-header__inner,
  .site-header .wordmark { animation: none; }
  /* The shadow is a state, not a movement, so here it simply stands
     rather than being animated away with the rest. */
  .site-header::after { opacity: 1; }
}

/* Reading progress along the top edge of the band, driven by the
   document's own scroll timeline. No listener, no per-frame work.

   It has to be placed on the bleed column explicitly. The header is a
   canvas grid, `.canvas > *` puts every child in the `wrap` column, and
   an absolutely positioned grid child with a placement takes that cell
   as its containing block, not the header. Without this the bar starts
   at the wordmark instead of at the edge of the screen. */
.read-progress {
  grid-column: bleed;
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: var(--progress-h);
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left center;
}
@supports (animation-timeline: scroll()) {
  @keyframes read-progress {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
  .read-progress {
    animation: read-progress linear both;
    animation-timeline: scroll(root block);
  }
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--p-space-3);
  min-block-size: var(--tap-min);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-h5);
  letter-spacing: var(--ls-h4);
  white-space: nowrap;
}
/* Sized in em, not pixels, so it tightens along with the wordmark when
   the band goes compact instead of staying put while the name shrinks
   away from it. */
.wordmark__mark {
  flex: none;
  inline-size: 1.25em;
  block-size: 1.25em;
  fill: var(--color-accent);
}

.site-nav__list {
  display: flex;
  align-items: center;
  gap: var(--p-space-3);
}
.site-nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  min-inline-size: var(--tap-min);
  padding-inline: var(--p-space-3);
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--color-text-mute);
  transition: color var(--p-dur-1) var(--p-ease-out),
              background-color var(--p-dur-1) var(--p-ease-out);
}
/* The needle runs under the label, the same gesture the work rows and
   every text link already use. The tinted pill it replaces was left over
   from the floating-capsule header and was the one hover on the site
   that had nothing to do with the stitch.

   The rule is inset to the text rather than the padding box, so it
   measures the word and not the tap target. */
.site-nav__link::after {
  content: "";
  position: absolute;
  inset-block-end: 12px;
  inset-inline: var(--p-space-3);
  block-size: var(--p-border-med);
  background-image: repeating-linear-gradient(
    to right,
    var(--color-accent) 0 var(--seam-run),
    transparent var(--seam-run) calc(var(--seam-run) * 2)
  );
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--p-dur-2) var(--p-ease-out);
}
/* Focus gets exactly what the pointer gets. A keyboard reader should
   never be told less than a mouse reader. */
.site-nav__link:hover::after,
.site-nav__link:focus-visible::after { transform: scaleX(1); }
.site-nav__link:hover { color: var(--color-text); }

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--p-space-2);
}

/* Square controls at the tap minimum. No glass of their own: inside the
   bar it would be a surface on a surface, and .icon-button is shared
   with the dialogs where there is no bar at all. */
.theme-toggle,
.icon-button,
.menu-button {
  display: inline-grid;
  place-items: center;
  inline-size: var(--tap-min);
  block-size: var(--tap-min);
  border-radius: var(--radius-pill);
  color: var(--color-text-mute);
  transition: color var(--p-dur-1) var(--p-ease-out),
              background-color var(--p-dur-1) var(--p-ease-out),
              transform var(--p-dur-1) var(--p-ease-out);
}
/* Barely there, and that is the point. These four are pressed often
   enough that anything larger would start to feel slow, but a control
   with no answer at all feels dead under a finger, where there is no
   hover to fall back on. */
.theme-toggle:active,
.icon-button:active,
.menu-button:active { transform: scale(0.94); }
.theme-toggle:hover,
.icon-button:hover,
.menu-button:hover {
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
}

/* `justify-content` is not decoration here. The shared control rule
   above centres these with `place-items`, which a column flex box only
   honours on the cross axis, so without this the two bars sat at the
   top of the 44px button and the icon rode 13px above the wordmark. */
.menu-button {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: var(--p-space-1);
}
.menu-button__bar {
  inline-size: 20px;
  block-size: var(--p-border-med);
  background: currentColor;
}

/* Deliberately the smallest control on the page. It is a standing
   offer, not the thing the visitor came to press: the hero's own CTA
   is 48px with a wide pad, this is 36 with a narrow one, and the gap
   between them is the hierarchy. The pseudo-element restores the 44px
   hit area that the visible box gives up. */
.site-header .site-header__cta {
  /* The button is its own positioning context. Without this the hit-area
     pseudo-element below resolves against .site-header__inner instead and
     stretches an invisible sheet across the whole band, swallowing every
     click meant for the wordmark, the nav and the theme switch. */
  position: relative;
  min-block-size: 36px;
  padding-inline: var(--p-space-4);
  font-size: var(--t-meta);
  font-weight: 600;
  white-space: nowrap;
}
.site-header .site-header__cta::before {
  content: "";
  position: absolute;
  inset-block: -4px;
  inset-inline: 0;
}

/* ── Hero · CINEMATIC ──────────────────────────────────────────
   One canvas, inset from the window edge so it reads as an object
   rather than as the page itself. The photograph fills it, the
   statement sits on a scrim in the bottom-left, a ticker runs the
   bottom edge.

   The canvas is dark in BOTH themes on purpose. It is a printed
   cover, not a UI surface, so it carries its own foreground and
   accent values rather than following the theme underneath. */
.cine {
  --inset: var(--canvas-inset);
  /* Dark in both themes, so both are stated here rather than inherited. */
  --cine-bg: #0E0E10;
  --cine-fg: #EDE7DC;
  --cine-mute: #A6A6AE;
  /* madder-500 lands near 2.7:1 on this canvas and fails outright.
     madder-300 is the value the dark theme already uses for accent
     text and clears 4.5:1 here. */
  --cine-accent: #E4614A;

  position: relative;
  isolation: isolate;
  /* Flush to the header at the top, inset on the other three sides, so
     the canvas hangs off the band rather than floating clear of it. */
  margin: 0 var(--inset) var(--inset);
  /* Sits below the header, not under it. The height is whatever is left
     of the window once the band and the two insets are taken out, so
     the header and the hero together are exactly one screen and nothing
     spills past the fold. */
  border-radius: var(--radius-canvas);
  overflow: hidden;
  background: var(--cine-bg);
  color: var(--cine-fg);
  min-block-size: min(calc(100svh - var(--nav-h) - var(--inset)), 880px);
  display: grid;
  /* minmax(0, 1fr), not the implicit auto column: the ticker's track is
     deliberately wider than the screen, and an auto column would size
     itself to that track and drag the statement's box out with it. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: 1fr auto;
}

.cine__media { position: absolute; inset: 0; z-index: -2; }
/* The <picture> has to be told to fill. It is an inline wrapper with no
   height of its own, so a percentage height on the <img> inside it has
   nothing to resolve against and the image silently falls back to its
   intrinsic ratio. On a landscape canvas that looks almost right, which
   is how it hid; on a tall phone canvas the photograph collapsed to a
   band across the top. */
.cine__media picture {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}
/* The source is now rendered 16:9, which is within a hair of the shape
   the canvas actually is, so `cover` trims almost nothing and the browser
   scales nothing. The subject sits right of centre in the frame and the
   statement sits bottom-left, so the two do not compete for the same
   corner and the default centring is correct. */
.cine__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: 50% 45%;
}

/* Three scrims, three jobs: a bed for the type along the bottom, a
   wash from the left so the corner stays dark whatever the crop does,
   and a light veil at the top to keep the nav legible. */
.cine__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(to top, rgb(14 14 16 / 0.92) 0%, rgb(14 14 16 / 0.55) 34%, transparent 68%),
    linear-gradient(105deg, rgb(14 14 16 / 0.85) 0%, rgb(14 14 16 / 0.35) 42%, transparent 72%),
    linear-gradient(to bottom, rgb(14 14 16 / 0.55) 0%, transparent 22%);
}

.cine__body {
  align-self: end;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--p-space-5);
  padding: clamp(var(--p-space-6), 4vw, var(--p-space-9));
  /* Lifted clear of the ticker. Sitting on the foot of the canvas made
     the whole block read as a caption under the photograph; carrying it
     up moves its centre from about 62% of the canvas to about 55%, which
     is nearer the middle without pretending to be centred. */
  padding-block-end: calc(clamp(var(--p-space-6), 4vw, var(--p-space-9))
                          + clamp(24px, 6vh, 80px));
  max-width: 46rem;
}

.cine__headline {
  font-family: var(--font-display);
  font-weight: 700;
  font-optical-sizing: auto;
  font-size: clamp(2.5rem, 5.6vw, 5rem);
  line-height: 0.98;
  letter-spacing: -0.04em;
  text-wrap: balance;
}
.cine__headline em { font-style: normal; color: var(--cine-accent); }

/* The headline arrives word by word, once per visit, rising into place
   from behind its own line.

   Nothing here hides the sentence on its own. The markup is a plain
   heading and the wrapping is done by scripts/main.js, which sets
   `data-split` in the same breath; every rule below is behind that
   attribute, so with scripting unavailable the headline simply stands.

   `clip` and not `hidden`: `hidden` would make every word a scroll
   container, and a `view()` timeline anywhere inside would then bind
   to a box two centimetres wide instead of to the page. */
.cine__headline[data-split] .word-line {
  display: inline-block;
  overflow: clip;
  vertical-align: bottom;
  /* The clip box is grown a fifth of an em and pulled back by the same
     amount, so the layout does not move. Line height here is 0.98,
     tighter than the font's own box, and clipping on the line itself
     would cut the tails off `g` and `y`. */
  padding-block: 0.2em;
  margin-block: -0.2em;
}
.cine__headline[data-split] .word {
  display: inline-block;
  /* Past 100% because the clip box is taller than the line by the
     padding above; at exactly 100% the top of the word would already
     be showing before it starts. */
  transform: translateY(125%);
  opacity: 0;
  transition: transform var(--p-dur-3) var(--p-ease-out),
              opacity var(--p-dur-2) var(--p-ease-out);
}
.cine__headline[data-split="played"] .word { transform: none; opacity: 1; }

.cine__sub {
  font-size: var(--t-lead);
  line-height: 1.5;
  color: var(--cine-mute);
  max-width: 40ch;
  text-wrap: pretty;
}

.cine__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--p-space-3);
}
/* The ghost button has no page background behind it here, so it grows
   its own surface instead of relying on a border alone. */
.cine .btn--ghost {
  background: rgb(255 255 255 / 0.08);
  border-color: rgb(255 255 255 / 0.22);
  color: var(--cine-fg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.cine .btn--ghost:hover { border-color: rgb(255 255 255 / 0.5); }

/* ── Availability chip ─────────────────────────────────────────
   The one decorative dot on the site, and it earns it: it marks a
   live state, which is the only thing a status dot is for. Wording
   is lifted from the Contact section, so it claims nothing the page
   does not already say. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--p-space-2);
  padding: 6px var(--p-space-4) 6px var(--p-space-3);
  border-radius: var(--radius-pill);
  border: var(--p-border-hair) solid rgb(255 255 255 / 0.16);
  background: rgb(255 255 255 / 0.08);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--cine-fg);
  white-space: nowrap;
}
.chip__dot {
  position: relative;
  inline-size: 7px;
  block-size: 7px;
  border-radius: var(--radius-pill);
  background: #4CB379;
  flex: none;
}
/* The dot stays, the pulse does not. The dot marks a real state, which
   is the one thing a status dot is for. The ring around it marked
   nothing and simply moved, and it was the second endless loop on a
   page that already runs a ticker. One of those reads as an accent,
   two read as restlessness. */

/* ── Ticker ────────────────────────────────────────────────────
   Kept to the bottom edge, where it reads as a footer to the canvas
   rather than as something competing with the statement. One per
   page, and this is it. */
.marquee {
  position: relative;
  overflow: hidden;
  border-block-start: var(--p-border-hair) solid rgb(255 255 255 / 0.14);
  background: rgb(14 14 16 / 0.5);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  padding-block: var(--p-space-3);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}
.marquee__track {
  display: flex;
  inline-size: max-content;
  animation: marquee 38s linear infinite;
}
.marquee__group {
  display: flex;
  align-items: center;
  gap: var(--p-space-6);
  padding-inline-end: var(--p-space-6);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--cine-mute);
  white-space: nowrap;
}
/* The separator is the module, not a bullet or a diamond. */
.marquee__group i {
  inline-size: var(--stitch-unit);
  block-size: var(--stitch-unit);
  background: var(--color-accent);
  flex: none;
}
@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
/* The global reduced-motion rule collapses durations, which would
   leave the ticker frozen mid-slide and the dot mid-pulse. Both have
   to be stopped outright, not shortened. */
@media (prefers-reduced-motion: reduce) {
  .marquee__track,
  .chip__dot::after { animation: none !important; }
}

@media (max-width: 767px) {
  /* A chapter opener outranks an ordinary section title by 52 to 40 on
     a desktop, a clear third again. Both clamps bottom out at 375px,
     four pixels apart, and the tier all but vanishes. Ordinary titles
     come down to 26 so the step survives the phone. Not to 24: a
     section title and the item titles under it are seen together on a
     phone, the two chapters almost never are, so the step that has to
     stay honest is 26 over the 20px item, not 32 over the section. */
  .section__title { font-size: 1.625rem; }
  /* Restated, not inherited. The chapter rule sits earlier in this file
     at the same specificity, so the line above would otherwise flatten
     the chapters into the ordinary titles and remove the very tier it
     is here to protect. */
  .section__title--chapter { font-size: var(--t-d2); }
  .cine { min-block-size: min(calc(100dvh - var(--nav-h-mobile) - var(--inset)), 720px); }
  /* A phone crops the wide frame hard, so hold onto the face rather
     than the middle of the room. */
  /* A 16:9 photograph inside a phone-shaped canvas has to be cropped
     more than three times over, and every version of that crop is an
     extreme close-up rather than a portrait. So on a phone the picture
     stops trying to be the background and becomes a band across the top
     at something near its own proportions; the statement sits below it
     on the plain canvas, where it needs no scrim at all. */
  .cine__media {
    inset-block-end: auto;
    block-size: 46%;
  }
  .cine__media img { object-position: 62% 36%; }
  .cine__scrim {
    inset-block-end: auto;
    block-size: 46%;
    background: linear-gradient(to top,
      rgb(14 14 16) 0%,
      rgb(14 14 16 / 0.45) 42%,
      rgb(14 14 16 / 0.10) 80%,
      transparent 100%);
  }
  .cine__actions .btn { flex: 1 1 auto; justify-content: center; }
}
/* ── Footer ────────────────────────────────────────────────────
   The name, a way back up, and the copyright. Nothing else: every
   other line that used to be here was already on the page, most of
   them several times over. */
.site-footer { padding-block: var(--space-block-y) var(--p-space-8); }
/* Wherever the sticky contact bar exists it is fixed to the bottom of
   the screen, so it paints over the last line of the footer. Measured
   at 375px: the legal line ended 1px under the bar even before the
   rhythm came down. The footer now reserves the bar's own height plus
   one step, so the line clears it by 32px.

   Stated after the base rule rather than in the mobile block earlier in
   this file, which would lose the cascade at equal specificity. */
@media (max-width: 1023px) {
  .site-footer {
    padding-block-end: calc(
      var(--p-space-6) + var(--sticky-cta-h) + env(safe-area-inset-bottom)
    );
  }
}

.site-footer__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--p-space-5);
}

.site-footer__name {
  display: flex;
  align-items: center;
  gap: var(--p-space-3);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-h4);
  letter-spacing: var(--ls-h4);
}
/* The stitch mark. It lived only in the browser tab until now, which
   meant the mark and the wordmark never stood together anywhere and so
   never accumulated into one identity. */
.site-footer__mark {
  flex: none;
  inline-size: 22px;
  block-size: 22px;
  fill: var(--color-accent);
}

/* The one thing in the footer that does a job. The page runs to nearly
   ten thousand pixels, so after the contact surface a way back to the
   top is worth more than another statement of the name. */
.site-footer__up {
  display: inline-flex;
  align-items: center;
  gap: var(--p-space-2);
  min-block-size: var(--tap-min);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--color-text-mute);
  transition: color var(--p-dur-1) var(--p-ease-out);
}
.site-footer__up:hover { color: var(--color-text); }

.site-footer__legal {
  margin-block-start: var(--p-space-7);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--color-text-mute);
}

/* ── Mobile menu sheet ─────────────────────────────────────── */
.menu-sheet {
  border: 0;
  padding: 0;
  margin: 0;
  inline-size: 100%;
  max-inline-size: 100%;
  block-size: 100dvh;
  max-block-size: 100dvh;
  background: var(--color-bg);
  color: var(--color-text);
  /* Flex column so the list sits at the top and the ways to reach her
     hold the bottom of the screen, which is where the thumb is. */
  display: flex;
  flex-direction: column;
}
/* The author display above would show the sheet while it is closed,
   so the closed state is restated here. Same shape as .case. */
.menu-sheet:not([open]) { display: none; }
.menu-sheet::backdrop { background: rgb(0 0 0 / 0.5); }
.menu-sheet__head {
  flex: none;
  height: var(--nav-h-mobile);
  padding-inline: var(--grid-pad);
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-block-end: var(--p-border-hair) solid var(--color-line);
}
.menu-sheet__list {
  flex: 1;
  padding: var(--p-space-6) var(--grid-pad) 0;
  display: flex;
  flex-direction: column;
}
.menu-sheet__list a {
  display: flex;
  align-items: center;
  gap: var(--p-space-4);
  min-height: 56px;
  font-family: var(--font-display);
  font-size: var(--t-h4);
  font-weight: 600;
}
/* A stitched seam between the items, not a solid rule. The rule was the
   one place in the whole site that divided with a plain line. */
.menu-sheet__list li + li {
  padding-block-start: var(--p-border-med);
  background-image: repeating-linear-gradient(
    to right,
    var(--color-stitch) 0 var(--seam-run),
    transparent var(--seam-run) calc(var(--seam-run) * 2)
  );
  background-size: 100% var(--p-border-med);
  background-repeat: no-repeat;
  background-position: top left;
}

/* The counted mark, the same device the sections carry. It counts the
   four items of the menu, not their section numbers: Contact is the
   seventh section, and seven stitches in two columns build a block
   four rows tall, taller than the word it marks. The two counts are
   never seen together, because the section marks only appear from
   1280px up and this sheet only exists below 1024px.

   Half the unit the sections use. At full size the square read as a
   second object beside the word rather than as a mark on it. */
.menu-count {
  flex: none;
  display: grid;
  grid-template-columns: repeat(2, var(--menu-mark-unit));
  gap: var(--menu-mark-gap);
  /* A fixed lane, so the four words start on one axis whatever the
     count beside them. */
  inline-size: calc(var(--menu-mark-unit) * 2 + var(--menu-mark-gap));
}
.menu-count i {
  inline-size: var(--menu-mark-unit);
  block-size: var(--menu-mark-unit);
  background: var(--color-stitch);
}
.menu-count i:first-child { background: var(--color-stitch-active); }

/* Held at the bottom of the sheet. On a phone the header's Let's talk
   is hidden, so this is the only route to her from the menu. */
.menu-sheet__foot {
  flex: none;
  padding: var(--p-space-6) var(--grid-pad) var(--p-space-7);
  display: grid;
  gap: var(--p-space-3);
  justify-items: start;
}
.menu-sheet__mail {
  position: relative;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-lead);
  letter-spacing: var(--ls-d3);
  overflow-wrap: anywhere;
}
/* The line box is 28px tall, so the address reaches its 44px through a
   hit area rather than through padding, which would push the links
   away from it and break the pair. Same move as .contact__mail. */
.menu-sheet__mail::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block: -10px;
}
.menu-sheet__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--p-space-5);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  color: var(--color-text-mute);
}
.menu-sheet__links a {
  display: inline-flex;
  align-items: center;
  gap: var(--p-space-2);
  min-height: var(--tap-min);
}
.menu-sheet__glyph {
  flex: none;
  inline-size: 1.25em;
  block-size: 1.25em;
  fill: currentColor;
}
.menu-sheet__glyph--stroke { fill: none; }

/* ── Mobile sticky contact ─────────────────────────────────── */
.sticky-cta {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--p-z-sticky);
  display: none;
  padding: var(--p-space-2) var(--grid-pad);
  padding-block-end: calc(var(--p-space-2) + env(safe-area-inset-bottom));
  background: var(--nav-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-block-start: var(--p-border-hair) solid var(--color-line);
}
.sticky-cta .btn { width: 100%; }
/* It arrives from the bottom edge and leaves by the same one, which is
   the only place it has ever been. The offset is the bar's own height
   rather than a number, so it stays right whatever the safe area under
   it turns out to be on a given phone.

   `display` needs `allow-discrete` because the bar is only laid out
   inside the mobile query below; without it the slide would be thrown
   away in the same frame it started. */
.sticky-cta {
  translate: 0 0;
  opacity: 1;
  transition: translate var(--p-dur-2) var(--p-ease-out),
              opacity var(--p-dur-2) var(--p-ease-out),
              display var(--p-dur-2) allow-discrete;
}
.sticky-cta:not([data-visible="true"]) { translate: 0 100%; opacity: 0; }

/* ══ Responsive ═══════════════════════════════════════════════ */

@media (max-width: 1023px) {
  .site-nav { display: none; }
  .menu-button { display: inline-flex; }
  /* The sticky bar at the foot of the screen already carries contact on
     a phone; two of them would be the same offer twice. The header
     class is needed because .btn sets its own display in a stylesheet
     that loads after this one. */
  .site-header .site-header__cta { display: none; }
  .site-header__inner { height: var(--nav-h-mobile); }
  #main { padding-block-start: var(--nav-h-mobile); }
  .site-header { --nav-h: var(--nav-h-mobile); --nav-h-compact: 50px; }
  .sticky-cta[data-visible="true"] { display: block; }
  @starting-style {
    .sticky-cta[data-visible="true"] { translate: 0 100%; opacity: 0; }
  }
}
