/* ==========================================================================
   WORK — the project grid
   CLAUDE.md §9. Six projects, two columns, staggered.
   ========================================================================== */

/* ONE SOURCE OF TRUTH FOR THE LINE AND ITS CLEARANCE.
   --rail-x is where the rail sits on narrow viewports; js/spine.js reads this
   same value rather than computing its own, so CSS and the drawn line cannot
   disagree. --rail-gutter is the clearance every element keeps from the line,
   used by the label and by every part of every stack so they stay aligned to
   one edge. Tune either here and everything follows. */
.work {
  position: relative;
  z-index: var(--z-content);
  padding-block: var(--sp-section);
  --rail-x: clamp(0.9rem, 4.5vw, 1.75rem);

  /* ONE CLEARANCE, BOTH LAYOUTS. The stop dot is the same size at every
     width (radius 14.7px, from the stroke), and the hover field outsets
     --wash-pad past the content toward the line, so the free space between
     them is (clearance - pad) in the centred layout and exactly the same
     subtraction against the rail on narrow. Deriving both from one value is
     what stops them drifting apart: the desktop corridor was fixed first and
     the narrow rail still overlapped the dot by 7.4px at 375, because the
     two were separate numbers that happened to be checked at different
     widths. */
  --wash-pad: clamp(0.85rem, 1.6vw, 1.6rem);
  --field-clear: calc(var(--wash-pad) + clamp(1.75rem, 2.4vw, 2.25rem));

  --rail-gutter: var(--field-clear);

  /* The stop dot's radius, in px, matching what js/spine.js draws: the dot is
     2.1x the stroke width and the stroke is --spine-w, so this is the one
     place the two representations of the same mark have to agree. */
  --dot-r: calc(var(--spine-w) * 2.1);
}

/* Centred on the LINE's starting position, not on the page. Its own container
   is centred and padded, so "centre the box" is not the same as "centre on
   the line" — js/spine.js measures the difference and publishes it as
   --head-shift once it has solved the route. */
.work__head {
  margin-bottom: var(--sp-2xl);
  text-align: center;
  transform: translateX(var(--head-shift, 0px));
}

/* base.css caps every <p> at --measure (62ch). Left as a block with that cap
   and no auto margins, the eyebrow sat left-aligned inside a centred head and
   landed 153px off the line. */
.work__head .eyebrow {
  max-width: none;
  font-size: clamp(1.05rem, 2.1vw, 2rem);
  letter-spacing: 0.26em;
}


/* --- The sequence --------------------------------------------------------
   ONE PROJECT PER ROW, FULL WIDTH, ALTERNATING SIDES.
   §9 asked for a staggered two-column grid specifically to avoid the uniform
   3x2 that reads as a template. An alternating full-width sequence achieves
   that more directly and gives each project a whole row of its own rather
   than a cell in a matrix — image left / text right, then image right / text
   left, down the page.

   It also fixes the arrival clustering for free. Two columns meant two cards
   at nearly the same height, so the line reached them in near-pairs no matter
   how far the stagger was pushed. One project per row makes the spacing
   between arrivals uniform by construction. */

.work__grid {
  display: grid;
  grid-template-columns: 1fr;
  /* Re-tuned after the covers grew. At 20vw the gap was 256px against a
     621px card at 1280 — 41% of the card's own height, which read as drift
     rather than separation, because it was set when cards were half as tall.
     14vw puts it near 30%: each project still gets its own moment without the
     page feeling like it has run out of content. */
  row-gap: clamp(var(--sp-2xl), 14vw, calc(var(--sp-3xl) * 1.4));
  list-style: none;
  margin: 0;
  padding: 0;
}

/* --- Card ----------------------------------------------------------------
   Each project is a vertical stack — coil and title on one line, subtitle
   under it, cover image under that — and the whole stack sits on one side of
   the line, alternating down the page. Not side-by-side: the stack reads as
   one block, so the image belongs under its own title rather than across a
   gutter from it.

   Source order is text then image, matching the visual order, so a screen
   reader hears the project named before its picture.

   The li is the two-column frame; the link is the stack and occupies one
   column of it. That keeps the alternation on the container and leaves the
   stack itself simple. */

/* COVER WIDTH IS DECOUPLED FROM THE TEXT MEASURE.
   Previously the whole stack — type and cover together — sat inside one
   column, so widening the cover meant widening the measure with it and the
   type sprawled. They are now sized independently: the cover fills its
   column and bleeds outward toward the viewport edge, while the text keeps a
   narrow measure centred inside that same column.

   The column gap is the line's clearance, so the covers grow until they are
   a set distance from the centre rather than being sized by a guess. */
.card {
  /* --wash-pad is inherited from .work, which derives --field-clear from it.
     Redeclaring it here would let the field and the clearance disagree. */
  /* How close a cover may come to the line. */
  --line-clear: clamp(1.6rem, 5vw, 5.5rem);
  /* How far it may bleed past the container toward the viewport edge —
     everything except a small outer margin. */
  --cover-bleed: calc(var(--gutter) - clamp(0.6rem, 1.4vw, 1.4rem));
  --text-measure: 34ch;

  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: calc(var(--line-clear) * 2);
}

.work__grid > li:nth-child(odd)  .card__link { grid-column: 1; }
.work__grid > li:nth-child(even) .card__link { grid-column: 2; }

/* The cover bleeds outward, away from the line, toward the viewport edge. */
.work__grid > li:nth-child(odd)  .card__media { margin-inline-start: calc(-1 * var(--cover-bleed)); }
.work__grid > li:nth-child(even) .card__media { margin-inline-end:   calc(-1 * var(--cover-bleed)); }

/* The text/cover centring correction that used to live here is retired. It
   existed because both sat in one column and the cover's outward bleed moved
   its centre away from the text's; across the line they are in different
   columns and there is no shared centre left to reconcile. */

.card__link {
  display: grid;
  /* The highlight field is outset by --wash-pad, so the gap has to clear that
     outset before it is a gap at all — otherwise the rectangle's bottom edge
     lands flush on the cover, which is what it did. Derived from the pad
     rather than guessed, so the two cannot drift apart. */
  row-gap: calc(var(--wash-pad) + clamp(0.4rem, 0.9vw, 1.1rem));
  grid-row: 1;
  color: inherit;
  text-decoration: none;
  outline-offset: 4px;
}

.card__media { background: var(--surface); }

.card__media img { transition: transform var(--t-slow) var(--ease); }

.card__link:hover .card__media img,
.card__link:focus-visible .card__media img { transform: scale(1.02); }


/* --- Text block ----------------------------------------------------------- */

/* --wash-pad is the inset between the type and the edge of the highlight
   field. It is applied by growing the field OUTWARD rather than padding the
   text inward: padding would push the type off the edge its cover is aligned
   to, which is the alignment the stack depends on. Declared on .card so the
   stack's row gap can be derived from it. */
/* Option C: centred within its block, in a measure of its own. The cover is
   free to be much wider without dragging the type out with it. */
.card__text {
  position: relative;
  isolation: isolate;
  display: grid;
  justify-items: center;
  text-align: center;
  row-gap: var(--sp-3xs);
  width: min(var(--text-measure), 100%);
  margin-inline: auto;
  padding: var(--sp-xs) 0 var(--sp-2xs);
  transition: color var(--t-quick) var(--ease);
}

/* Coil and title share one line, with the index tucked above the title. */
/* One centred column: coil above the index, index above the title, so the
   head reads as a single vertical alignment rather than a mark hung off the
   side of the type. */
.card__head {
  display: grid;
  justify-items: center;
  grid-template-columns: 1fr;
  grid-template-areas: 'coil' 'index' 'title';
  row-gap: var(--sp-2xs);
}

.card__head .card__index { grid-area: index; }
.card__head .card__title { grid-area: title; }

.card__coil {
  grid-area: coil;
  display: block;
  /* Smaller. At 56-92px it competed with the title for the eye; the coil is
     a marker for the stop, not the headline of the project. */
  width: clamp(30px, 3.2vw, 52px);
  aspect-ratio: 3 / 4;
  align-self: center;
}

.card__coil svg { width: 100%; height: 100%; overflow: visible; }

/* VARIANT A — the full conch. The outer-sweep crop is kept behind these two
   values as the documented fallback: set --coil-crop to "0 450 550 1000" and
   --coil-start to 550 to restore it. */
.card__coil {
  --coil-crop: 1000 1000;
  --coil-start: 1000;
}

.coil__art { fill: var(--kumkumam); }

.coil__crop,
.coil__mask {
  fill: none;
  stroke: #fff;
  stroke-width: 32;
  stroke-linecap: round;
}

.coil__crop { stroke-dasharray: var(--coil-crop); }

.coil__mask {
  stroke-dasharray: 1000 1000;
  stroke-dashoffset: 0;
}


/* --- Card text and the wash ----------------------------------------------
   §9: "Kumkumam wash bleeds in from the left on hover, text inverting to
   paper." The wash is a pseudo-element scaled from the left edge rather than
   a background-position animation, so it composites on the GPU and does not
   repaint the text under it.

   It is scoped to the text block, not the whole card: washing over the cover
   image would obscure the work, which §1 puts above any interaction. */

.card__text::before {
  content: '';
  position: absolute;
  /* Outset on every side, so nothing in the stack — coil, index, title,
     subtitle or year — ever sits against the boundary of the field. */
  inset: calc(-1 * var(--wash-pad));
  z-index: -1;
  background: var(--kumkumam);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--t-quick) var(--ease);
}

.card__link:hover .card__text::before,
.card__link:focus-visible .card__text::before {
  transform: scaleX(1);
}

.card__link:hover .card__text,
.card__link:focus-visible .card__text {
  color: var(--paper);
}

/* The coil inverts with the type it belongs to. */
.card__link:hover .coil__art,
.card__link:focus-visible .coil__art {
  fill: var(--paper);
}

/* Every text colour inside the body has to invert together, or the subtitle
   stays --ink-muted on kumkumam — 2.6:1, which fails AA. */
.card__link:hover .card__sub,
.card__link:focus-visible .card__sub,
.card__link:hover .card__year,
.card__link:focus-visible .card__year,
.card__link:hover .card__index,
.card__link:focus-visible .card__index {
  color: var(--paper);
}

.card__index {
  display: block;
  font-size: var(--type-micro);
  letter-spacing: var(--ls-wide);
  color: var(--kumkumam);
  transition: color var(--t-quick) var(--ease);
}

/* Sized to fit on ONE line at every viewport — the longest title is
   "Integrated Campaigns". The stack is half the grid wide, so this is much
   smaller than --type-title; text-wrap: nowrap is deliberately NOT used,
   because silently overflowing would be worse than wrapping if a longer
   title is ever added. Verified single-line across the range instead. */
.card__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.4rem, 2.5vw, 2.5rem);
  line-height: 1.04;
  letter-spacing: var(--ls-display);
}

/* Larger than before: with the title reduced, the subtitle carries more of
   the description in this arrangement. */
.card__sub {
  display: block;
  margin-top: var(--sp-2xs);
  max-width: none;
  font-size: clamp(1.0625rem, 1.75vw, 1.45rem);
  line-height: 1.34;
  color: var(--ink-muted);
  transition: color var(--t-quick) var(--ease);
}

/* RECEDES BY TYPE, NOT BY AN ILLEGAL CONTRAST. This carried the retired
   faint tone, which measured 2.07:1 on 12px text — below both AA bars. It now
   takes the same --ink-muted as the subtitle and stays a clearly lower tier
   through size (half the subtitle), tracking (0.26em against the subtitle's
   normal) and the space above it. */
.card__year {
  display: block;
  margin-top: var(--sp-s);
  font-size: var(--type-micro);
  letter-spacing: 0.26em;
  color: var(--ink-muted);
  transition: color var(--t-quick) var(--ease);
}


/* The card-6 placeholder rules that lived here are gone. They were the other
   consumer of the retired faint tone, and the markup they styled no longer
   exists — card 6 carries the Supra still now. */


/* --- Reduced motion -------------------------------------------------------
   The wash carries the hover state, so it is kept — removing it would leave
   no affordance at all. Only the image scale goes, which is decoration. */

@media (prefers-reduced-motion: reduce) {
  .card__media img { transition: none; }
  .card__link:hover .card__media img,
  .card__link:focus-visible .card__media img { transform: none; }
}


/* ==========================================================================
   THE CROSS-LINE LAYOUT — DESKTOP ONLY

   The cover sits on one side of the centre line and the text block on the
   other, alternating down the spine: image-left/text-right, then
   image-right/text-left. The line runs between them rather than beside a
   stack that happens to sit on one side of it.

   SCOPED BY A COMPLEMENTARY MEDIA QUERY, NOT BY SPECIFICITY, and that is the
   whole point of writing it this way. On narrow the line is a left rail with
   a single column of content to its right — there is no opposite side to move
   anything to, so this arrangement is not merely unnecessary there, it is
   impossible. Specificity has silently lost this argument repeatedly in this
   build (a:hover, ul[role='list'], h3 colour), so the guard here is
   structural: `not all and (max-width: 46rem)` is the exact complement of the
   narrow block in spine.css. Neither can apply at a width the other covers,
   and there is no gap between them where a third behaviour could appear.
   ========================================================================== */

@media not all and (max-width: 46rem) {

  /* The link spans both columns and becomes the two-column grid itself. Not
     `display: contents` on the anchor — that has historically dropped
     interactive elements out of the accessibility tree, and this element is
     the card's only link. */
  /* :nth-child(n) is load-bearing. The base rules above are
     `.work__grid > li:nth-child(odd) .card__link` at (0,3,1); a plain
     `.work__grid > li .card__link` is (0,2,1) and loses, which left the link
     inside one column and made the covers SMALLER instead of larger.
     Matching the specificity and coming later in the file is what wins. */
  .work__grid > li:nth-child(n) .card__link {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: calc(var(--line-clear) * 2);
    align-items: center;
    row-gap: 0;
  }

  /* Placement is explicit, so source order stays text-then-media: a screen
     reader hears the project named before its picture, whichever side the
     picture is on. */
  .work__grid > li:nth-child(odd)  .card__media { grid-column: 1; grid-row: 1; }
  .work__grid > li:nth-child(odd)  .card__text  { grid-column: 2; grid-row: 1; }
  .work__grid > li:nth-child(even) .card__media { grid-column: 2; grid-row: 1; }
  .work__grid > li:nth-child(even) .card__text  { grid-column: 1; grid-row: 1; }

  /* FREED ROOM, SPENT — and bounded by arithmetic rather than by taste.
     With a line down the centre, each cover can occupy at most
       (viewport / 2) - (edge margin) - (clearance from the line)
     so the ceiling is fixed by the composition, not by the CSS. Three dials
     move it and all three are set to the edge of what stays comfortable:

       - the card outsets past the container to sit ~18px from the viewport
         edge, instead of a full gutter in
       - the line's clearance drops from 5vw to 3vw a side
       - the cover fills its column outright, so no separate bleed is needed

     Anything past this needs the line to stop being centred, which is a
     different composition and not what was asked for. */
  /* `li.card`, not `li .card`. The <li> IS the card — a descendant selector
     matched nothing at all, so every value below silently fell back to the
     base rule and the covers came out narrower than before the change. */
  .work__grid > li.card {
    --card-outset: calc(var(--gutter) - clamp(0.75rem, 1.4vw, 1.5rem));
    /* CLEARANCE IS DERIVED FROM THE FIELD, NOT CHOSEN.
       The hover field outsets --wash-pad past the column toward the line, so
       the free corridor either side of the axis is (clearance - pad), and the
       stop dot's radius is a hard floor under it: at the old 3vw the corridor
       came out at 8px against a 14.7px radius on a 736 viewport, so the
       highlight ran straight into the circle. Expressing clearance as the pad
       plus a real gap makes the corridor a constant of the composition rather
       than something that happens to work at the width it was checked at.
       Cost is 13px of cover width at 1280 — 2%, and the circle is clear at
       every width instead of at none. */
    --line-clear: var(--field-clear);
    --text-measure: 40ch;
    margin-inline: calc(-1 * var(--card-outset));
  }

  /* The cover now fills its own column, so the outward bleed that used to
     widen it inside a shared column is off. Left on, it pushed the artwork
     57px past the viewport edge and cropped it. */
  .work__grid > li:nth-child(odd)  .card__media,
  .work__grid > li:nth-child(even) .card__media { margin-inline: 0; }

  /* Both up. Measured against the base rules rather than eyeballed: the
     title goes 32 -> 43.5 at 1280 and 40 -> 54.4 at 1920. The subtitle's
     first pass was 1.5vw, which came out at 19.2 against the base rule's
     22.4 — smaller, not larger, at exactly the width the change is for. */
  .card__title { font-size: clamp(2rem, 3.4vw, 3.4rem); }
  .card__sub   { font-size: clamp(1.15rem, 1.85vw, 1.75rem); }

  /* --- The highlight encloses both halves ------------------------------
     One region across the line rather than a field behind the type only.
     It is BEHIND the content, so the opaque cover paints over its own part
     and the field reads as a surround rather than as a tint over the work —
     §1 keeps anything that obscures an image off the table. */

  .card__link {
    position: relative;
    isolation: isolate;
  }

  /* THE FIELD WIPES IN FROM THE COVER'S SIDE.
     Direction is derived from where the cover sits, not from the index: the
     odd/even rules that place the cover also set --wipe-from, so if the
     alternation ever changes the wipe follows it instead of going stale.
     Cover left -> wipes left to right; cover right -> right to left. The
     motion starts at the image and runs toward the type, so the field reads
     as spreading out of the work rather than arriving from nowhere. */
  /* ONE FIELD ACROSS THE WHOLE CARD. The project and its cover light up
     together — they are one thing being hovered, and a gap down the middle
     read as two.

     The corridor that used to sit here solved a real problem the wrong way:
     it kept the stop dot out of the field by removing the field from around
     it. The line and the dot are now drawn back over the field in paper
     instead (below), so the mark stays legible without the field's edge
     cutting across it. */
  .card__link::before {
    content: '';
    position: absolute;
    inset: calc(-1 * var(--wash-pad));
    z-index: -2;
    background: var(--kumkumam);
    transform: scaleX(0);
    transform-origin: var(--wipe-from, left) center;
    transition: transform var(--t-quick) var(--ease);
  }

  /* Hover and focus only. The scroll-driven arrival state is untouched. */
  .card__link:hover::before,
  .card__link:focus-visible::before { transform: scaleX(1); }

  /* Paired with the placement rules above, so the two cannot disagree. */
  .work__grid > li:nth-child(odd)  .card__link { --wipe-from: left; }
  .work__grid > li:nth-child(even) .card__link { --wipe-from: right; }

  /* Reduced motion: the field is still there, it simply does not travel.
     scaleX is what animates, so removing the transition and starting at 1
     gives the highlight with no slide. */
  @media (prefers-reduced-motion: reduce) {
    .card__link::before,
    .card__link::after { transition: none; }
    .card__link:hover::before,
    .card__link:focus-visible::before { transform: scaleX(1); }
  }

  /* THE LINE AND ITS STOP, REDRAWN IN PAPER OVER THE FIELD.
     Both are --kumkumam, so a solid field of the same colour simply swallows
     them: the line vanishes for the height of the card and the dot — which
     marks this project's place on the route — vanishes with it.

     Two background layers on one element rather than two elements, because
     ::before is already the field. The bar is a 1-stop gradient sized to
     --spine-w, the same value the drawn stroke uses; the disc is a radial
     stop at the dot's real radius, centred on the axis at the card's top edge
     because that is where spine.js puts every stop ("level with the TOP of
     the stack"). --wash-pad is therefore the distance from the field's top
     edge down to the dot's centre.

     The half-pixel on the outer radial stop is antialiasing: an abrupt
     transparent stop leaves a hard, slightly ragged rim.

     It sits at 50% of the link, which is the axis the line is fitted to — the
     link spans the grid and spine.js centres the route on the grid — so the
     two cannot drift. */
  .card__link::after {
    content: '';
    position: absolute;
    inset: calc(-1 * var(--wash-pad));
    z-index: -1;
    background:
      radial-gradient(circle at 50% var(--wash-pad),
                      var(--paper) var(--dot-r),
                      transparent calc(var(--dot-r) + 0.5px)),
      linear-gradient(var(--paper), var(--paper));
    background-repeat: no-repeat;
    background-size: 100% 100%, var(--spine-w) 100%;
    /* THE BAR STARTS AT THE DISC, NOT AT THE FIELD'S EDGE.
       The field outsets --wash-pad above the card and the stop sits exactly
       on the card's top edge, so a bar drawn from the field's own top left
       (pad - radius) of paper standing above the circle — 5.8px at 1280 — and
       it read as a stray white tick rather than as the line.

       Nothing is lost by starting lower: above the disc the real line is
       kumkumam running into a kumkumam field, so it reads as continuing
       whether or not paper is painted over it. Where the radius exceeds the
       pad the value goes negative, the bar is clipped at the top edge, and
       there was no stub to remove in the first place. */
    background-position: 0 0, 50% calc(var(--wash-pad) - var(--dot-r));
    opacity: 0;
    transition: opacity var(--t-quick) var(--ease);
  }

  .card__link:hover::after,
  .card__link:focus-visible::after { opacity: 1; }

  /* The old field was a pseudo-element of the text block alone. It stays for
     narrow, where it is still correct, and is switched off here. */
  .card__text::before { display: none; }


}
