/* ==========================================================================
   home.css — compositions that exist only on the landing page.
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE GIANT WORD

   The composition is made with GRID PLACEMENT, not with transforms, negative
   margins or percentage offsets. A six-column, four-row grid is stretched
   across the whole band behind the word; each capture is dropped into named
   cells. Change one grid-column and the whole composition re-flows — there
   is no magic number to keep in sync.

   Stacking, back to front:  media (0) · dek (1) · word (2) · the one
   capture that crosses in FRONT of the closing letters (3).

   Note for the record: the benchmark never places imagery in front of its
   display word, and keeps its left two-thirds permanently clear. Both rules
   are broken here on purpose — the brief asks for a real interleave.
   -------------------------------------------------------------------------- */
/* Two rows, not one stack. Row 1 is the PLANE — the captures and the display
   word occupy the same cell and interleave there. Row 2 is the dek, on clean
   ground. The first composition let the dek float over row 1 and it landed on
   top of a photograph, unreadable. Nothing overlaps the dek by construction
   now: it is in a different grid row. */
.stage {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto auto;
  /* One step up the band scale from the old --space-2xl, because the g's
     descender is the first ink on this site that leaves its own line box:
     it overruns the media plane by 2% of the plane's height, and at
     --space-2xl the tail of the g was landing 40px off the dek. */
  row-gap: var(--space-2xl);
  /* NO top padding. The stage is the first thing after the opener rail, and
     its top air is the rail band's bottom padding — one token, applied once,
     rather than two bands of space stacked on each other. That stacking is
     what put the mark a screenful below the nav; with the statement block
     gone there is nothing left for it to be clearing. */
  padding-block: 0 var(--space-4xl);
}
/* The exception, and the reason the rule above is safe: option B moves the
   stage into the middle of the page, where it is a band like any other and
   takes a hairline. A band under a rule with no top padding puts the rule on
   the word. Symmetric, longhand (D1), because here it IS a band. */
.stage.rule-top { padding-block: var(--space-4xl) var(--space-4xl); }

.stage__word {
  grid-area: 1 / 1;
  align-self: center;
  position: relative;
  z-index: 2;
  /* optical: pull the e's left side bearing (4.2/100em, measured in the
     shipped face) back onto the grid edge. Paired with --display-fit, which
     lands the c's right ink edge on the opposite margin. */
  margin-left: -0.042em;
}

/* The dek sits on the SAME 1fr 2fr spine as every opener, footer and entry row
   on the site, so the stage does not invent a seam of its own. */
.stage__content {
  grid-area: 2 / 1;
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--grid-gap);
}
.stage__content > * { grid-column: 2; }

/* the media plane. It runs past the right page margin so the portrait can
   bleed off the edge of the page. No z-index here on purpose: the container
   must not open a stacking context, or the front capture could not rise
   above the word. */
.stage__media {
  /* Same cell as the word, so the two interleave; the negative right margin
     lets the portrait bleed to the viewport edge while the grid's left edge
     still lines up with the type. */
  grid-area: 1 / 1;
  margin-right: calc(var(--page-margin) * -1);
  /* The plane is exactly as tall as the word, by construction: the word's
     line-height is 1, so its box height IS its font size. Re-fit the word and
     the plane follows, which is what keeps the row placements below meaning
     the same thing at every size. */
  min-height: var(--display-size);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: repeat(12, 1fr);
  /* The gutter is the SAME token as every other layout column on the site, so
     two captures can never fuse into one continuous band. This was the bug in
     the first composition: a zero gap made three photographs read as one. */
  gap: var(--grid-gap);
  pointer-events: none;
}
/* Grid decides WHERE a capture sits; the ratio class decides what SHAPE it is.
   Neither is allowed to decide the other — which is how D6 (composition is
   grid placement) and D4 (a small fixed set of ratios) both hold at once. */
.stage__media figure {
  position: relative; overflow: hidden; border-radius: var(--radius); margin: 0;
}
.stage__media img, .stage__media video { width: 100%; height: 100%; object-fit: cover; }

/* Twelve columns, so a capture can be placed against the word rather than
   merely beside it. The placement below is re-cut for "egc" and could not be
   the placement for "Commerce": an eight-character uppercase word is a solid
   band of ink across the middle of the plane, and three lowercase letters
   with no ascender at all are a band of ink across the BOTTOM of it, under
   ~30% of empty line box.
   Measured in the shipped face at the shipped size, as fractions of the
   plane's height: x-height top at 32%, baseline at 86%, the g's descender
   bottom at 102% — i.e. the descender is the only ink that leaves the plane.

   A — top left, BEHIND, and the largest object in the plane after the word
   itself. It hangs from the plane's top edge (it spans every row, so its own
   ratio decides how far down it reaches, and it never drives the plane's
   height) and runs from the ascender void down THROUGH the letterforms: the
   constellation is a plane the word is printed on, not a stamp sitting above
   it. Square rather than 16:9 for the same reason it is bigger — the subject
   is a radial field, and a 16:9 frame spent 63% of its width on white that
   is indistinguishable from the page. Same ratio set (D4), same grid
   placement law (D6): only the cell and the class changed. */
/* The nudge is VERTICAL only. It used to be translate(-4%, -25%), and the
   horizontal half of that put the figure's box 4px left of the viewport edge
   on every page carrying the stage: clipped by the body (F2 held, nothing
   scrolled) but still an element escaping the page, which is what the QA
   sweep's escape check exists to catch. The constellation still sits where
   that nudge put it; it is moved inside its own frame instead, by the
   object-position below. Framing, like .stage__c's object-position: no
   filter, no wash, nothing moved in the composition. */
/* ROUND NINE: one column wider. The constellation was reading as a stamp
   rather than as the plane the word is printed on, and the stage has the
   room -- columns 8 and 9 are the gap between the graph and the portrait,
   and the portrait starts at 10. Taking column 8 closes half that gap and
   buys the graph ~14% of the plane's width at no cost to anything it must
   not touch: the word still crosses it, and .stage__b still starts three
   columns clear. */
.stage__a { grid-column: 1 / 9;  grid-row: 1 / 13; align-self: start;  z-index: 0; transform: translateY(-25%); }
/* The recording's last pixel ROW is #DDDDDD — a one-pixel edge baked into the
   capture. At 16:9 cover cropped the source vertically and threw that row
   away; a square frame crops horizontally instead, so the row lands exactly
   on the frame's bottom edge and draws a grey hairline across 800px of white
   page. Trim the encode's border rather than paper over it: the video box is
   4px taller than its frame and overflow: hidden takes 2px off each end.
   Framing, like .stage__c's object-position — no filter, no wash, no move. */
/* object-position carries the horizontal nudge that used to be a transform on
   the figure. cover already crops this 1600x1092 recording horizontally into a
   square frame, so there is ~380px of slack to move through. 66% shows the
   window ~61px to the right, which draws the constellation that much further
   left inside a frame that has not moved: the band's left edge is already the
   page inset, so "further left" can only be a crop, never a position. Same
   picture, same size, and nothing escapes the page. Was 58.5% for two rounds;
   the band gained a column in round nine and the extra width went to the
   white side of the recording rather than to the graph. */
.stage__a video { height: calc(100% + 4px); margin-block: -2px; object-position: 66% center; }
/* B — portrait, bleeds off the right edge, and the only element on the page
   that sits IN FRONT of the display word. It takes the top third of the c
   and stops: with three letters in the word, a c whose aperture is covered
   stops being a c, and the mark stops reading. The whole lower two-thirds of
   the letter, aperture included, stays clear.
   It is also the reason nothing behind it carries black type — this capture
   is a black garment on a light wall, and a black letterform drawn over it
   would have disappeared into the garment. */
.stage__b { grid-column: 10 / 13; grid-row: 1 / 13; align-self: center; z-index: 3; }
/* C — the descender moment, BEHIND, in the columns the g occupies. The g's
   tail is the one piece of the word that crosses a photograph and then runs
   out of the bottom of it onto clean ground. One gutter clear of B, and a
   row clear of A. */
.stage__c { grid-column: 4 / 9;  grid-row: 7 / 13; align-self: end;    z-index: 0; }
/* The plate in C is a 3:4 portrait in a 4:3 slot, so cover crops 44% of its
   height away. Centred, the crop cut the figure off at the ankles. This
   moves the visible band down the source so the whole figure and a little
   floor are inside it. Framing only — no filter, no scale, no layout move. */
.stage__c img { object-position: center 87%; }

/* Below the interleave breakpoint the composition becomes an honest stack —
   and on a phone it is a stack of TWO things: the constellation, then the
   word, then the line under it. The rack portrait and the concrete capture
   are not shrunk, they are dropped: at 350px they were two more rectangles
   under a mark that had already been made, and the desktop composition's
   whole argument (three captures interleaved WITH the letterforms) cannot
   survive a single column anyway. What survives is the one piece of media
   that is about the work rather than about a garment.

   The order inverts here. On desktop the constellation is the plane the word
   is printed on; in a column the honest translation of "behind" is "first",
   so the graph arrives, then the mark, then the line that names the studio.

   It has to become a FLEX column to do that. The desktop composition places
   the word and the media plane in the same explicit grid cell, and `order`
   is ignored on an explicitly-placed grid item — so the mobile rules here
   used to declare an order that never took effect, the word stayed in the
   plane's cell, and the front capture (z-index 3, full width on a phone)
   painted straight over it. The site's monumental wordmark was invisible on
   every phone. Clearing grid-area is what makes order mean anything.
   -------------------------------------------------------------------------- */
@media (max-width: 768px) {
  .stage {
    display: flex;
    flex-direction: column;
    gap: var(--space-xl);
    /* same rule as the desktop composition: the rail band's bottom padding is
       the stage's top air, and there is only one of it */
    padding-block: 0 var(--space-2xl);
    min-height: 0;
  }
  .stage.rule-top { padding-block: var(--space-2xl) var(--space-2xl); }
  .stage__word, .stage__media, .stage__content { grid-area: auto; }
  /* align-self: center is a VERTICAL centring on the desktop grid and a
     HORIZONTAL one in this column flex container, which quietly centred the
     wordmark on every phone. */
  .stage__word { order: 2; align-self: stretch; }
  .stage__content { order: 3; display: block; }
  .stage__media {
    order: 1;
    /* no bleed on a phone: the stack sits on the page inset like everything
       else, rather than running off one edge only */
    margin-right: 0;
    min-height: 0;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    gap: var(--grid-gap);
  }
  /* Vertical only, for the same reason as the desktop rule above: the 5%
     horizontal half of this used to push the figure's box past the right edge
     of the viewport, which showed up as 3px of real overflow at 480 and 16px
     at 768. shot.mjs samples 390 and 1440 and never saw it; the width ladder
     did. The nudge itself is kept, in the video's own margins below. */
  .stage__a { grid-column: 1; grid-row: auto; align-self: auto; z-index: auto; transform: translateY(5%); }
  /* Both dropped, not resized. See the note above the breakpoint. */
  .stage__b, .stage__c { display: none; }
  /* Full inset width, and the constellation fills it. The recording is a
     circle about 55% of the source height centred in a frame that is 45%
     dead white on every side; at 350px across, a frame that shows all of
     that white draws the graph 192px wide, which is the small stamp this
     round exists to kill. So crop INTO the recording's own margin rather
     than grow the frame: a 3:4 frame would buy the same 300px of
     constellation and spend 117px of stage height on white that is
     indistinguishable from the page it sits on. The frame is square here,
     which is why one percentage sizes both axes and one margin centres
     both. Framing only, like .stage__c's object-position on desktop: no
     filter, no wash, nothing moved in the composition. max-width is released
     because the base reset's job (media never escapes its container) is done
     here by the figure's own overflow: hidden. */
  /* object-position goes back to centre here: the desktop value carries a
     horizontal nudge that belongs to the desktop composition, and this frame
     is already cropping into the recording's own margin from all four sides.
     The asymmetric side margins are that crop moved 5% to the right, which is
     the nudge the figure's transform used to carry: left and right still sum
     with the 160% width to 100%, so nothing is scaled and nothing escapes. */
  /* ROUND NINE: 200% and symmetric. Two defects, one rule.
     SIZE -- at 160% the recording still spends a third of the square frame on
     white that is indistinguishable from the page, which drew the graph at
     roughly 230px on a 390px screen: the same small stamp the previous round
     set out to kill, killed only halfway. 200% crops further into the
     recording's own margin and gives the constellation the full inset width.
     CENTRING -- the old side margins were -25% left against -35% right, which
     is the desktop composition's horizontal nudge carried onto a phone where
     nothing else in the composition survives to be nudged away from. It put
     the graph 5% right of the frame it sits in, and on a centred column that
     reads as a mistake rather than as a placement. The margins are equal now,
     so the frame's centre and the recording's centre are the same point.
     Left and right still sum with the width to 100%: nothing escapes. */
  .stage__a video { width: 200%; height: 200%; max-width: none; margin: -50%; object-position: center; }
}

/* --------------------------------------------------------------------------
   THE MEDIA OPENER  (option B)

   A page that opens on a capture instead of on a sentence. It is the one
   band on the site allowed to break D2's "first band clears the nav with one
   nav-height of air": the whole point is that the work starts almost at the
   top edge. It is still a multiple of the header token rather than a new
   number — one and a half nav heights, so 35px of air clears the rail.
   -------------------------------------------------------------------------- */
.band-open { padding-block: calc(var(--header-height) * 1.5) var(--space-4xl); }

/* --------------------------------------------------------------------------
   THE INDEX GROUP HEAD  (option C)

   A section label living INSIDE a list rather than above a band. It takes
   the same .h5 step every section label on the site takes; only the spacing
   is new, and both numbers are off the band scale.
   -------------------------------------------------------------------------- */
.index__head {
  padding-block: var(--space-2xl) var(--space-sm);
  border-bottom: 1px solid var(--border);
}
.entries > .index__head:first-child { padding-top: var(--space-lg); }

/* --------------------------------------------------------------------------
   SERVICES — a band of four that drifts on its own.

   The RESTING state of this component is the static four-across row it has
   always been. Drift is added on top, by JS, and only when the reader has not
   asked for reduced motion — so with no script, or with reduced motion, this
   is four columns on one row and every card is on screen at once. That is
   also why the second set of cards is written into the markup as plain
   duplicated copy carrying aria-hidden: it only exists to make the loop
   seamless, and in the resting state it is simply not displayed.

   The drift itself is one `translate` on the track, driven by rAF and wrapped
   modulo one set width — so the hand and the machine move the same number
   and the band is infinite in both directions. A scroll container plus a CSS
   animation cannot do that: drag far enough and you reach the end of the
   duplicated set and find white space.
   -------------------------------------------------------------------------- */
.marquee {
  border-top: 1px solid var(--border);
  padding-top: var(--space-lg);
  overflow: hidden;
}
.marquee__set {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--grid-gap);
}
.marquee__set + .marquee__set { display: none; }
/* No card ordinal. Four cards in a drifting row have no first and no fourth —
   the band has no ends — so 01 to 04 was numbering a sequence that does not
   exist (round seven). */
.service { display: flex; flex-direction: column; gap: var(--space-xs); align-items: flex-start; }

/* drift mode — added by JS, never by the stylesheet alone */
.marquee.is-drifting { cursor: grab; }
.marquee.is-drifting.is-held { cursor: grabbing; }
.marquee.is-drifting .marquee__track { display: flex; width: max-content; will-change: translate; }
.marquee.is-drifting .marquee__set {
  display: flex;
  /* the trailing gutter is what makes set N's last card sit one gutter from
     set N+1's first card, which is what makes the wrap invisible */
  padding-right: var(--grid-gap);
}
.marquee.is-drifting .marquee__set + .marquee__set { display: flex; }
.marquee.is-drifting .service { flex: 0 0 var(--card); }

@media (max-width: 1024px) { .marquee__set { grid-template-columns: repeat(2, 1fr); row-gap: var(--space-2xl); } }
@media (max-width: 600px)  { .marquee__set { grid-template-columns: 1fr; row-gap: var(--space-xl); } }

/* --------------------------------------------------------------------------
   FULL-BLEED MEDIA
   -------------------------------------------------------------------------- */
.bleed {
  width: calc(100% + var(--page-margin) * 2);
  margin-inline: calc(var(--page-margin) * -1);
}
.bleed__figure { position: relative; overflow: hidden; background: var(--ground-2); }
.bleed__figure video, .bleed__figure img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Art direction, not a shrink: at 2:1 across a 390px phone the capture is a
   178px letterbox and the swatch grid, the yarn codes and the price — the
   whole point of the shot — are unreadable. On a phone the frame becomes 4:3
   and holds the right-hand control panel, which is where the numbers live. */
@media (max-width: 768px) {
  .bleed__figure.r-21 { aspect-ratio: 4 / 3; }
  .bleed__figure.r-21 video { object-position: 82% center; }
}
.bleed__caption {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-xl);
  padding-top: var(--space-xs);
}

/* --------------------------------------------------------------------------
   THE REEL — framed still, click to play. Never autoplays.
   -------------------------------------------------------------------------- */
.reel { display: grid; grid-template-columns: 2fr 1fr; gap: var(--grid-gap); align-items: start; }
.reel__player {
  position: relative;
  display: block;
  width: 100%;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--ground-2);
  cursor: pointer;
}
.reel__player img, .reel__player video { width: 100%; height: 100%; object-fit: cover; }
.reel__player video {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity var(--anim-md);
}
.reel__player.is-playing video { opacity: 1; }
/* The ONE filled object left on this site, and the reason it survives the
   flat law: it does not sit on the page ground. The flat law is "black text
   on white"; a photograph has no white to put black text on, and this
   poster is mid-grey concrete, so neither ink nor white text clears it
   unaided. A control laid over media is therefore the single exemption —
   and it is the only place a fill appears anywhere on the site. */
.reel__play {
  position: absolute;
  left: var(--space-md);
  bottom: var(--space-md);
  z-index: 2;
  display: inline-flex;
  padding-inline: var(--space-2xs);
  padding-block: var(--space-3xs);
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--on-ink);
  transition: opacity var(--anim-sm);
}
.reel__player.is-playing .reel__play { opacity: 0; }
.reel__side { display: flex; flex-direction: column; gap: var(--space-md); align-items: flex-start; }
/* Measure in em, like every other measure on the site (B6). The column is
   ~450px at 1440, which at the serif step is already close to right; the
   cap is what keeps the two entries' columns the same shape at 1024, where
   the stack puts this text across the whole measure. */
.reel__side .serif { max-width: 20em; }
.reel__side .caption { max-width: 34em; }
.reel__plate {
  display: flex; flex-direction: column; gap: var(--space-2xs);
  border-top: 1px solid var(--border); padding-top: var(--space-xs);
  width: 100%;
}
@media (max-width: 1024px) { .reel { grid-template-columns: 1fr; gap: var(--space-xl); align-items: start; } }

/* --------------------------------------------------------------------------
   THE WORK STAGE — one slot, N entries, paged.

   This replaces the three-slide migration panel, and it takes that panel's
   construction wholesale because it was the right construction: one grid
   cell, `visibility` rather than `display`, so the stage is as tall as the
   tallest entry, a page turn moves nothing below it, and the hidden entry is
   out of the tab order and out of the accessibility tree with no ARIA
   bookkeeping to drift out of step with the class.

   ADDITIVE (F5). The resting state — no script, and under reduced motion
   with the script — is the two entries stacked in flow on a hairline, which
   is the shape this band has always had. `is-paged` is the single switch, in
   the stylesheet and in the loop; take it away and the stack comes back,
   controls and all.

   REDUCED MOTION keeps the controls and the paging and loses only the clock
   and the crossfade, exactly as the migration panel did.
   -------------------------------------------------------------------------- */
/* resting: a stack */
.work + .work {
  border-top: 1px solid var(--border);
  padding-top: var(--space-3xl);
  margin-top: var(--space-3xl);
}

/* paged: one cell */
.works.is-paged .works__frame {
  display: grid;
  /* The browser keeps the vertical scroll; the stage claims the horizontal,
     which is what makes a swipe possible without stealing the page. */
  touch-action: pan-y;
}
.works.is-paged .work {
  grid-area: 1 / 1;
  border-top: 0; padding-top: 0; margin-top: 0;
  visibility: hidden;
  opacity: 0;
}
.works.is-paged .work.is-current { visibility: visible; opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  /* the middle step of the ladder: 0.8s is "something opened" */
  .works.is-paged .work { transition: opacity var(--anim-md); }
}
/* While a finger is on the stage the outgoing entry must not be mid-fade, or
   a drag that is abandoned leaves the page half-lit. */
.works.is-held .work { transition: none; }

.works__bar { display: none; }
.works.is-paged .works__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-md);
  border-top: 1px solid var(--border);
  padding-top: var(--space-md);
  margin-top: var(--space-2xl);
}

/* Prev and Next are the site's one link idiom: a hairline under the label, an
   arrow that steps 3px, and quiet brightening to ink. No fill, no box (A7). */
.works__step {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5em;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color var(--anim-sm);
}
.works__step:hover { color: var(--ink); }
.works__step .link__label { border-bottom: 1px solid var(--border); padding-bottom: 0.2em; }
.works__step:hover .link__label { border-color: var(--ink); }
.works__step .link__arrow { transition: translate var(--anim-sm); }
.works__step:hover .link__arrow { translate: -3px 0; }
.works__step--next:hover .link__arrow { translate: 3px 0; }
/* A control that cannot do anything says so by going quiet, not by
   disappearing: a bar whose ends move around as you page is worse than a
   bar with a spent end on it. */
.works__step[disabled] { color: var(--border); cursor: default; }
.works__step[disabled] .link__label { border-color: var(--border); }

/* Position. NOT dots: a dot is a filled object, and this site has exactly one
   of those and it lives on a photograph (A7). The entry numbers are the
   position, shown the way the nav shows the current page and the signal rail
   shows the current signal — ink, with an ink hairline under it. They are
   also direct access, which a dot is only by accident. */
.works__rail { display: flex; align-items: baseline; gap: var(--grid-gap); }
.works__dot {
  color: var(--ink-muted);
  cursor: pointer;
  border-radius: 0;
  border-bottom: 1px solid transparent;
  padding-bottom: 0.25em;
  transition: color var(--anim-sm), border-color var(--anim-sm);
}
.works__dot:hover { color: var(--ink); }
.works__dot[aria-current="true"] { color: var(--ink); border-bottom-color: var(--ink); }

/* --------------------------------------------------------------------------
   CONNECT
   -------------------------------------------------------------------------- */
.connect__word { margin-block: var(--space-lg) var(--space-xl); }
/* The terminal action of the site. It was the one filled button left after
   the chips came off, which made a small black rectangle the last object on
   every page — an odd thing to be the loudest mark on a page with no other
   filled mark anywhere. It is now the same hairline idiom as every other
   link, one type step up so it still reads as the destination rather than as
   a footnote. Tap target comes from the 0.5em block padding, not from a box.
   Nothing on the page ground is filled now; the only fill on this site is
   .reel__play, and that one sits on a photograph. */
.mailto {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  /* the type step comes from the .h6 class in the markup, so the fluid
     engine is declared exactly once, where it always is */
  padding-block: 0.5em;
  color: var(--ink);
}
.mailto .link__label {
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.2em;
  transition: border-color var(--anim-sm);
}
.mailto:hover .link__label { border-color: var(--ink); }
.mailto .link__arrow { transition: translate var(--anim-sm); }
.mailto:hover .link__arrow { translate: 3px 0; }
