/* ═══════════════════════════════════════════════════════════════════
   SECTIONS
   Paper for the statement, then two employer grounds tinted from the
   work itself — the burgundy of the sweater in the film, WW's blue —
   and ink to close.
   ═══════════════════════════════════════════════════════════════════ */

/* line-reveal scaffolding, written by js/sections.js.
   Same descender problem as the hero: tight leading puts ink outside the
   line box, so the mask gets padding and gives the space back. */
.rl__ln {
  display: block;
  overflow: hidden;
  padding-bottom: 0.13em;
  margin-bottom: -0.13em;
}
.rl__in { display: block; }

/* ═══════════════════ 02 · STATEMENT ═══════════════════ */
.stmt {
  padding-block: clamp(8rem, 25vh, 18rem) clamp(8rem, 25vh, 18rem);
  background: var(--paper);
}
.stmt__lede {
  font-size: var(--stmt-fs);
  line-height: 0.9;
  letter-spacing: -0.022em;
  color: var(--ink);
  max-width: var(--stmt-measure);
}

/* ── the statement that rewrites itself ──────────────────────────
   Driven by initMorph() in js/sections.js. Nothing here hides anything:
   with the script broken both halves simply sit there, which is the rule
   for everything below the fold.

   One screen tall so the pin has a stage to hold, and centred in it. The
   sentence gains a line when the longer half swaps in, so the script
   reserves the taller of the two on the block — otherwise centring would
   jolt the whole thing upward at the moment of the swap.

   `min-height` and not `height`: on a short screen the sentence still has
   to fit, and clipping a line off it would be the worse failure. */
.stmt--morph {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding-block: clamp(6rem, 13vh, 10rem);
  overflow: clip;
}

/* a word can only be moved on its own if it has a box of its own.
   Shared by the word reveals and by the rewrite. */
.rl__w { display: inline-block; }

/* The origin note, set as a counterweight to the lede rather than as a
   caption under it: the same size and the same run, pushed to the right
   and turned to face the right edge, with enough air above to read as a
   separate block. A <p>, so the display face and the caps that h2 gets
   for free have to be asked for here. */
.stmt__origin {
  font-family: var(--f-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: var(--stmt-fs);
  line-height: 0.9;
  letter-spacing: -0.022em;
  color: var(--ink);
  width: var(--stmt-measure);
  margin-inline-start: auto;                  /* over to the right */
  margin-top: clamp(8rem, 30vh, 22rem);       /* a long breath after the lede */
  text-align: right;
}

/* ═══════════════════ 03/04 · EXPERIENCE ═══════════════════
   One film, two panels. It spans both grid rows so it can stay sticky
   across the whole region, and it is half a column wide so a single
   translateX carries it from the Lyft side to the WW side. The ground
   colour is crossfaded by js/sections.js over the same range.        */

.exp {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
  background: var(--paper);
  color: var(--ink);

  /* the grounds are the page's own paper now; the hues survive only as
     accents — the sweater's burgundy, WW's blue */
  --exp-a-ink: var(--wine);
  --exp-b-ink: var(--navy);

  /* How far the film falls short of its own half of the grid.

     The frame is sized by its own ratio, not by the column — `.exp__frame`
     takes `min(100%, 100svh × ratio)` so it is never cropped — so on a
     wide, short window it is narrower than the half it sits in and leaves
     a band of empty paper beside it. Measured at 1910×930: a 749px film
     in a 955px column, 206px of nothing.

     That band is invisible, but it is not nothing to look at. Centring
     the words in the *column* then centres them in a box whose edge
     nobody can see — 190px of gap on each side by the numbers, 396px
     against 189px to the eye. `--lean` is half that band, and each panel
     shifts by it toward its film so the words sit in the middle of the
     space that is actually there.

     It resolves to zero whenever the film does fill its column, which is
     most windows: at 1440×900 the ratio wants 725px and the column is
     720, so there is nothing to correct and nothing moves. */
}

.exp__media {
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  align-self: start;
  position: sticky;
  top: 0;
  width: 50%;
  height: 100vh;
  height: 100svh;
  display: grid;
  /* hugs the near edge; the far edge takes any slack */
  place-items: center start;
  z-index: 1;
  will-change: transform;
}
.exp__media.is-far { place-items: center end; }

/* Sized by each film's own ratio rather than the column's, so nothing is
   ever cropped — on a wide, short window the column is the wrong shape
   and `cover` was eating the character's head. Both frames occupy the
   same grid cell; only the live one is painted. */
.exp__frame {
  grid-area: 1 / 1;
  position: relative;
  width: min(100%, calc(100svh * var(--film)));
  aspect-ratio: var(--film);
  max-height: 100svh;
  overflow: hidden;
  background: #12130D;
  visibility: hidden;
  /* the shutter — js drives --shutter from 0% to a slit and back */
  clip-path: inset(0 var(--shutter, 0%) 0 var(--shutter, 0%));
}
.exp__frame.is-live { visibility: visible; }

.exp__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* where the clip is up to — the loop restarts rather than ping-pongs, so
   this is what makes the cut read as a restart instead of a glitch */
/* Inset rather than flush to the frame edge: the frame is nearly viewport
   height, so a flush bar lands in the last two pixels of the screen and
   reads as an artifact instead of an instrument. */
.exp__timer {
  position: absolute;
  left: clamp(0.9rem, 2vw, 1.5rem);
  right: clamp(0.9rem, 2vw, 1.5rem);
  bottom: clamp(0.9rem, 2vh, 1.5rem);
  height: 2px;
  background: rgb(233 226 210 / 0.22);
  overflow: hidden;
}
.exp__timer i {
  display: block;
  width: 100%;
  height: 100%;
  background: rgb(233 226 210 / 0.8);
  transform: scaleX(0);
  transform-origin: left;
}

.exp__panel {
  position: relative;
  z-index: 2;
  padding-block: clamp(7rem, 20vh, 14rem);
}
/* The tail of one and the head of the other are held apart, because the
   film has to travel across the middle and would otherwise cross over a
   column that still has words in it. js/sections.js crossfades the two
   over the same range to cover the handover. */
.exp__panel--a {
  grid-column: 2; grid-row: 1;
  --accent: var(--exp-a-ink);
  padding-bottom: 45vh;
}
/* The trailing room is not symmetry for its own sake. WW is the last
   panel, so when its grid row ends the film unsticks and the next section
   takes the screen — and measured at 1440×900 the WW column's top reached
   the top of the film with **47px** of scroll to spare. It arrived at the
   moment it was swept away, which is why the whole panel read as rushed.

   The bottom padding is what the film stays stuck through, so this buys
   the column somewhere to go: it climbs to the top of the frame beside it
   and is held there for about half a screen before the section releases.
   Lyft never needed it — its own 45vh gap below is the handover, and it
   has the whole of WW's approach to travel through. */
.exp__panel--b {
  grid-column: 1; grid-row: 2;
  --accent: var(--exp-b-ink);
  padding-top: 45vh;
  padding-bottom: 55vh;
}

.exp__panel { will-change: transform, opacity; }
/* Every size in this panel is `clamp(floor, min(vw, vh), cap)`.

   The `min()` is what makes it answer to the screen rather than only to
   its width: a wide, short window is the case that breaks display type,
   and taking whichever of the two dimensions is tighter is what stops the
   panel outgrowing the film beside it.

   The coefficients are cut so 1280×800 — a 13" laptop — lands on exactly
   the sizes it had before, `vw = px/12.8` and `vh = px/8`, and the caps
   were then lifted well clear so the growth carries on past that. They
   used to bind almost immediately: the title gained 6% between 1280 and
   1920 while the screen gained 50%, which is why a large display made
   this column look shrunken. The measure grows with it, or the bigger
   words simply wrap tighter and nothing is gained. */
/* A fixed measure, centred in its half of the grid.

   Both halves of that matter. The measure is fixed because the rules in
   here are `border-bottom`s — the eyebrow, the role line, every work row
   — so they are exactly as wide as the block is, and a block that grows
   with the screen turns them into rules that stretch across the panel
   with nothing under them. Capped, they hold one length: 576px of rule at
   1440 and the same at 2560.

   And centred, because the two panels sit on opposite sides of the film
   and used to lean towards it — 72px of padding on the film side against
   60px on the screen side. `margin-inline: auto` with one padding value
   puts each block in the middle of its own column, so both read the same
   way and the rules under them start and end level. */
.exp__inner {
  max-width: 46rem;
  margin-inline: auto;
  padding-inline: clamp(1.75rem, 5vw, 5rem);
}
/* Each panel leans toward its own film by half the dead band, which is
   what puts the words in the middle of the gap you can see rather than
   the middle of the column. `translate` and not `transform`, so this
   cannot be clobbered by anything animating the panel above it. */
.exp__panel--a .exp__inner { translate: calc(-1 * var(--lean)) 0; }
.exp__panel--b .exp__inner { translate: var(--lean) 0; }

.exp__panel .sect-idx {
  color: color-mix(in oklab, var(--ink) 46%, transparent);
  border-bottom-color: color-mix(in oklab, var(--ink) 16%, transparent);
}

.exp__title {
  font-size: clamp(3.825rem, min(11.05vw, 17.7vh), 15rem);
  line-height: 0.8;
  letter-spacing: -0.04em;
  color: var(--ink);
}

/* deliberately large — this is the line people actually scan for */
.exp__role {
  margin-top: 1.1rem;
  font-family: var(--f-ui);
  font-size: clamp(1.105rem, min(1.867vw, 2.99vh), 2.4rem);
  font-weight: 600;
  letter-spacing: 0.055em;
  text-transform: uppercase;
  line-height: 1.25;
  color: var(--accent);
  padding-bottom: clamp(1.75rem, 4.5vh, 2.75rem);
  border-bottom: 1px solid color-mix(in oklab, var(--ink) 16%, transparent);
}

/* one line, at a size you can actually read */
.exp__prose { padding-top: clamp(1.75rem, 4.5vh, 2.75rem); }
.exp__prose p {
  font-size: clamp(1.16875rem, min(1.484vw, 2.375vh), 1.9rem);
  line-height: 1.5;
  color: color-mix(in oklab, var(--ink) 78%, transparent);
  text-wrap: nowrap;
}
@media (max-width: 78rem) { .exp__prose p { text-wrap: pretty; } }

.exp__work {
  list-style: none;
  margin-top: clamp(2.5rem, 6vh, 4rem);
  border-top: 1px solid color-mix(in oklab, var(--ink) 16%, transparent);
}
.exp__work li { border-bottom: 1px solid color-mix(in oklab, var(--ink) 16%, transparent); }

.exp__work-link {
  position: relative;
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr);
  align-items: baseline;
  gap: 1.1rem;
  padding: clamp(1.15rem, 2.8vh, 1.8rem) 0;
  overflow: hidden;
}

.exp__work-idx {
  font-family: var(--f-ui);
  font-size: clamp(0.74375rem, min(0.93vw, 1.4875vh), 1.1rem);
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--accent);
  transition: transform 0.55s var(--ease), opacity 0.35s var(--ease);
}
/* the index steps aside for an arrow on approach */
.exp__work-idx::after {
  content: ' ↗';
  opacity: 0;
  transition: opacity 0.35s var(--ease);
}

.exp__work-name {
  position: relative;
  display: inline-block;
  font-family: var(--f-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: clamp(2.04rem, min(4.672vw, 7.475vh), 6rem);
  line-height: 0.94;
  letter-spacing: -0.018em;
  color: color-mix(in oklab, var(--ink) 82%, transparent);
  transition: transform 0.6s var(--ease), color 0.4s var(--ease);
}
/* A second copy of the word, wiped across from the left. attr() keeps it
   in the CSS rather than duplicating the markup.

   It arrives in the panel's own ink — burgundy for Lyft, blue for WW —
   which costs nothing to do per panel because `--accent` is already
   scoped to each one for the role line and the row indices. So the wipe
   reads as the row taking on the colour of the employer it belongs to,
   and the same two rules serve both columns. */
.exp__work-name::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  color: var(--accent);
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.62s var(--ease);
  pointer-events: none;
}

/* a hairline that draws under the row in the same beat, and leaves out
   the right the way every other underline on the page does */
.exp__work-rule {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.7s var(--ease);
}

.exp__work-link:hover .exp__work-name,
.exp__work-link:focus-visible .exp__work-name {
  transform: translateX(clamp(0.35rem, 1vw, 0.9rem));
}
.exp__work-link:hover .exp__work-name::after,
.exp__work-link:focus-visible .exp__work-name::after { clip-path: inset(0 0 0 0); }
.exp__work-link:hover .exp__work-idx::after,
.exp__work-link:focus-visible .exp__work-idx::after { opacity: 1; }
.exp__work-link:hover .exp__work-rule,
.exp__work-link:focus-visible .exp__work-rule {
  transform: scaleX(1);
  transform-origin: left;
}

/* Held shut by js/study.js until the studies have something in them.
   Refusing the pointer is what does it: no tap, and no hover either, so
   the wipe and the rule stay down rather than promising a destination
   that is not there yet. Flip `LIVE` in js/study.js to undo all of it. */
.exp__work-link.is-idle { pointer-events: none; }
.exp__work-link.is-idle .exp__work-idx::after { opacity: 0; }

@media (hover: none) {
  /* no hover to lean on, so the affordance is the arrow, always shown */
  .exp__work-idx::after { opacity: 1; }
}

/* ── phone: one column, the film shown once, colour carried by the
   panels themselves rather than by a crossfade ─────────────────── */
@media (max-width: 60rem) {
  /* Nothing sits beside anything down here, so there is no band to lean
     into. It would not be zero on its own: a phone held sideways is wide
     and very short, which is exactly the shape that makes the film fall
     short of half the width. */
  .exp { grid-template-columns: 1fr; --lean: 0px; }
  /* `display: contents` lets the two frames become grid items in their own
     right, so each employer keeps its own film above its own words rather
     than one film standing in for both. */
  .exp__media {
    display: contents;
    position: static;
    transform: none !important;
  }
  .exp__frame {
    grid-column: 1;
    grid-area: auto;
    position: relative;
    width: 100%;
    max-height: none;
    visibility: visible;
    clip-path: none;
  }
  .exp__frame[data-exp-frame="a"] { grid-row: 1; }
  .exp__panel--a { grid-column: 1; grid-row: 2; }
  .exp__frame[data-exp-frame="b"] { grid-row: 3; }
  .exp__panel--b { grid-column: 1; grid-row: 4; }
  /* nothing slides, so the handover gap is not needed */
  .exp__panel,
  .exp__panel--a,
  .exp__panel--b {
    padding-block: clamp(4rem, 12vh, 7rem);
    opacity: 1 !important;
    transform: none !important;
  }
  .exp__inner,
  .exp__panel--a .exp__inner,
  .exp__panel--b .exp__inner { padding-inline: var(--gutter); max-width: none; }
}

/* ═══════════════════ 05 · CONTACT ═══════════════════ */
/* ── the bouncing marks ───────────────────────────────────────────
   The playing field is the rule above the social links, made literal.

   Its left and right edges are the rule's own, so the marks never stray
   into the page margin — the rule spans the `.wrap` content box, which
   is the viewport less `--gutter-wide` at each end, and this box is inset
   to match. Its floor is the rule itself: `initFooterTrail()` sets the
   height to exactly that line.

   `overflow` is what makes the floor solid *and* disposes of the marks.
   A mark bounces off the rule and then drops through it, and there is
   nothing below the line to see it — so nothing ever turns up down in
   the sign-off, and no cleanup depends on a tween finishing.

   Behind the type: they fall past the headline and the address rather
   than over them, so nothing you came here to read is ever covered. That
   takes a z-index on the content too — an absolutely positioned child
   paints above a static sibling by default, so leaving `.wrap` alone
   would put the marks in front however low this one goes. */
.end__drop {
  position: absolute;
  top: 0;
  left: var(--gutter-wide);
  right: var(--gutter-wide);
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.end > .wrap {
  position: relative;
  z-index: 1;
}
/* Both dimensions come from the script, which reads each file's own ratio
   — so a 3:4 portrait and a square can sit in the same list without
   either being squashed. Deliberately *not* `height: auto`: a fresh <img>
   has no intrinsic size until the file is decoded, and auto resolves to
   zero in that window. Measured, the first marks came out 170 across and
   10 tall. */
.end__drop img {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  /* Rounded hard. These are the only photographs on the page and the
     corners are what stop them reading as screenshots dropped on a
     layout — at 170px across this is about an eighth of the width. It
     rides the pop and the squash with the rest of the transform. */
  border-radius: 22px;
  will-change: transform;
}

/* Ink to close, and permanently so — the one dark ground on a paper
   page. `relative`, because the drop layer above measures itself
   against this box. */
.end {
  position: relative;
  --bg:   var(--ink);
  --fg:   var(--paper);
  --fg-2: color-mix(in oklab, var(--fg) 66%, var(--bg));
  --fg-3: color-mix(in oklab, var(--fg) 38%, var(--bg));
  --rule: color-mix(in oklab, var(--fg) 20%, var(--bg));
  --accent: #C9694C;

  /* The air on both sides of the closing row, declared once so the two
     cannot drift apart. Above it: from "Get in touch." down to the rule.
     Below it: from the row down to the sign-off. They were 108 and 88
     and the difference showed — the headline sat further off the rule
     than the row sat off the name, which reads as the rule belonging to
     the row rather than sitting between the two. */
  --end-breath: clamp(3.5rem, 11vh, 8rem);

  background: var(--bg);
  color: var(--fg);
  padding-block: clamp(5rem, 13vh, 9rem) clamp(2rem, 5vh, 3rem);
}

/* The footer stands on the chrome's inset rather than the page's, so
   "Get in touch." starts on exactly the left edge the name in the nav
   does and the links end where the clock does — the page opens and
   closes on the same two lines.

   That means coming off the shared column as well: `.wrap` is centred at
   --maxw while the chrome is full-bleed, so a footer left on the column
   parts company with it on any screen wider than that — 262px against
   the nav's 149px at 1920. Measured off the viewport instead, like the
   chrome. Scoped to the footer's own wrap, so the token and every other
   section that reads it are untouched. */
/* The footer's own inset override is gone: `.wrap` stands on
   `--gutter-wide` uncapped for every section now, which is what this rule
   used to arrange for the footer alone. */

/* The blob the heading reaches for. It lives inside `.end__big` so the
   goo filter on that element has both it and the letters to work on, and
   outside `.rl__ln` so the line clip that masks the reveal does not eat
   it. `currentColor`, because the merge only reads as one substance if
   the two shapes are the same colour to the pixel.

   Sized and moved from js/sections.js — see initGooHeading(). */
.end__goo {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  border-radius: 50%;
  background: currentColor;
  pointer-events: none;
  opacity: 0;
  will-change: transform;
}

.end__big {
  /* the blob above is positioned against this */
  position: relative;
  /* height-capped too: two lines of this at full width is the tallest
     thing on the page after the statement */
  font-size: clamp(5.6rem, min(21.6vw, 37.5vh), 22.4rem);
  line-height: 0.8;
  letter-spacing: -0.035em;
  color: var(--fg);
  /* The same breath above the rule as below it — see `--end-breath`. */
  margin-bottom: var(--end-breath);
}

/* Set as a third social link, because that is what it is now: same face,
   size, weight, tracking and colour as LinkedIn and Instagram, and the
   same lift to white on hover. The one difference is that its underline
   is already there — an address is worth marking as reachable before you
   point at it — so the hover brightens a line that exists rather than
   drawing one that does not. */
.end__mail {
  display: block;
  position: relative;
  width: fit-content;
  max-width: 100%;
  justify-self: start;
}
.end__mail-text {
  display: block;
  font-family: var(--f-ui);
  font-size: var(--row-fs);
  font-weight: 600;
  /* The one thing not matched to the links, and it is the address
     that forces it: 0.14em over twenty-five characters is 70px of
     pure tracking, a quarter of the string. Held at 0.02em so the
     row can stay at 1.25rem on a laptop instead of every item
     shrinking to 16.8px to accommodate the spacing between the
     letters of an email address. */
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fg-2);
  transition: color 0.35s var(--ease);
  white-space: nowrap;
}
/* Sits where the links' underline sits, so the three line up. Dimmer
   than the type on purpose: it is the resting state, and the hover has
   to have somewhere to go. */
.end__mail-rule {
  position: absolute;
  left: 0; bottom: -3px;
  width: 100%; height: 1px;
  background: var(--rule);
}
/* In from the left, out to the right — the origin flips the moment the
   pointer leaves, and only `transform` transitions, so the stroke keeps
   travelling the same way instead of retracing itself. Same 0.45s the
   links use, so pointing at any of the three feels like one gesture. */
.end__mail-rule::after {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s var(--ease);
}
/* White, the same lift the links take. */
.end__mail:hover .end__mail-text,
.end__mail:focus-visible .end__mail-text { color: #fff; }
.end__mail:hover .end__mail-rule::after,
.end__mail:focus-visible .end__mail-rule::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* Three across: address, colophon, links.

   A grid of three equal tracks, not `space-between`, because the middle
   item has to be centred on the *row* and not merely left with whatever
   space the other two did not take. With `space-between` the colophon
   would drift left or right by half the difference between the address
   and the links, which is exactly the kind of near-miss that reads as a
   mistake. Equal tracks centre it on the page's own axis, and the two
   ends are pushed to their edges from within their own tracks. */
.end__foot {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: center;
  gap: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);

  /* One size for all three, declared here so they cannot drift apart, and
     keyed to the *track* rather than the viewport.

     The address is what governs it. Twenty-five characters of `nowrap`
     type is about 15px of width for every 1px of size, and its track is
     only a third of the row — so at 20px it needs 296px and a 1280 window
     gives it 299. Below that it spills, silently: the anchor is
     `max-width: 100%` so its *box* clamps and the text runs on underneath
     the colophon, which no bounding-rect check catches. Measured before
     this: 57px of spill at 1280, 185px at 768.

     Dividing the track by 15 is that relationship inverted, so the row
     holds 1.25rem wherever it fits and steps down exactly as fast as it
     has to. The other two are short enough for any size, but shrinking
     the address alone is the mismatch this row was asked to remove — so
     all three read this and stay a set. */
  --row-track: calc((100vw - 2 * var(--gutter-wide) - 4rem) / 3);
  --row-fs: min(1.25rem, calc(var(--row-track) / 15));
}
/* Two of them, side by side against the right edge, and large enough to
   be the thing you reach for down here rather than a footnote beside the
   colophon. */
.end__links {
  list-style: none;
  display: flex;
  justify-content: flex-end;
  gap: 1.75rem;
}
.end__links a {
  font-family: var(--f-ui);
  font-size: var(--row-fs);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-2);
  position: relative;
  transition: color 0.35s var(--ease);
}
.end__links a::after {
  content: '';
  position: absolute;
  left: 0; bottom: -3px;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s var(--ease);
}
/* White rather than the page's bone: these sit at the dimmest step in the
   footer, so the hover has to clear `--fg` to read as a lift at all. */
.end__links a:hover,
.end__links a:focus-visible { color: #fff; }
.end__links a:hover::after,
.end__links a:focus-visible::after { transform: scaleX(1); transform-origin: left; }
/* ── room for a finger ───────────────────────────────────────────────
   The social links stand 24px tall and the address 32px, which is under
   what a thumb lands on reliably. The hit area is grown and nothing else:
   an absolutely positioned pseudo takes no space, so the type and the
   rule under it stay exactly where they are. `::after` is the underline
   on the links, so theirs goes on `::before`. */
@media (hover: none) {
  .end__links a::before,
  .end__mail::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 2.75rem;
    transform: translateY(-50%);
  }
}

/* The middle of the three, and centred on its own track so it lands on
   the page's axis. Same size as the links and the address, so the row
   reads as one row rather than a caption between two menus. */
.end__note {
  text-align: center;
  font-size: var(--row-fs);
  letter-spacing: 0.14em;
  color: var(--fg-2);          /* the links' colour, not the fainter label one */
}

/* ── the sign-off name, full width ─────────────────────────────────
   Plain: it takes no hover of its own. Sized to the column by js —
   `inline-block` so its box is the width of the text rather than the
   column's, which is what the fit measures against. */
.end__name {
  display: block;
  margin-top: var(--end-breath);      /* matched to the gap above the rule */
  line-height: 0.86;
}
/* One slot per letter, written by initEndName(). `overflow` is what makes
   a hovered letter leave *view* rather than slide over its neighbours.

   The vertical padding gives back the room that clipping would otherwise
   take: the line height here is 0.86, so the caps stand outside their own
   line box and a tight clip would shave their tops off. The negative
   margin cancels it again, so nothing moves. Every letter is a slot, so
   they all take the same baseline and stay on one line together. */
.end__ch {
  display: inline-block;
  overflow: hidden;
  padding-block: 0.25em;
  margin-block: -0.25em;
}
.end__ch-in { display: inline-block; }

.end__name-text {
  display: inline-block;
  font-family: var(--f-display);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.03em;
  line-height: 0.86;
  white-space: nowrap;
  font-size: 12vw;             /* fallback before the fit runs */
  color: var(--fg);
}

@media (max-width: 46rem) {
  /* One column, so the three stack in source order and all three take the
     left edge — a centred colophon over left-aligned neighbours reads as
     a mistake once there is no row to be the middle of. */
  .end__foot {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: 1.25rem;
    /* stacked, so each item has the whole column rather than a third */
    --row-track: calc(100vw - 2 * var(--gutter-wide));
  }
  .end__note { text-align: left; }
  .end__links { justify-content: flex-start; }
}
