/* ============================================================
   WORK GRID — the selected-work section as a two-up grid.
   Load AFTER redesign.css.

   Replaces the full-width case card and its sticky stack. The
   cover is the whole object, two to a row, and the copy sits
   underneath it as page text rather than inside a panel.

   No stack here, deliberately. Pinning needs a full-width card
   for the next one to slide over; a half-width tile has nothing
   to cover, so the rows would pin on top of each other with half
   the screen showing through.
   ============================================================ */

/* ---------- the grid ----------
   The row gap is more than double the column gap on purpose. Side by
   side the tiles are told apart by the channel between them, but
   stacked they are told apart by the space between one tile's last
   line and the next tile's cover — and that space has to be bigger
   than any space inside a tile, or the rows read as one long column
   of alternating pictures and paragraphs. */
.work-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-lg);
  row-gap: var(--space-3xl);
}

/* ---------- the tile ----------
   Not a card. No fill, no ring, no radius and no padding at this
   level: the only rounded rectangle in the whole tile is the cover
   itself, which is the entire point of the arrangement. */
.wg-card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  min-width: 0;
}

/* The frame exists so the CTA can sit over the cover WITHOUT living
   inside its clip. In the figure, `overflow: hidden` cropped the
   button's shadow against the cover's rounded edge, which read as a
   dark smear along the corner rather than as a lifted object. */
.wg-frame { position: relative; }

/* ---------- hover: the cover expands ----------
   The studio grid scales its whole card, but a studio card is nothing but
   image. This tile carries real prose under the cover, and scaling a tile
   scales its type: at 1.02 the description resamples and goes soft, which is
   the one thing a case description cannot afford. So the COVER takes the
   expand and the copy stays put and crisp.

   It grows from its own bottom edge, not its centre. Scaling from the middle
   pushes the cover down into the description — the gap under it is 20px and a
   2% growth on a ~250px cover spends 2.5px of that at each end — where a
   bottom origin sends the whole 5px upward, into the row gap, and leaves the
   distance between cover and title exactly as authored. Nothing below it
   moves, which is what keeps the text where the eye left it.

   1.02 and --dur-med are the studio card's own numbers. */
.wg-media {
  transform-origin: center bottom;
  transition: transform var(--dur-med) var(--ease-out-expo);
}
.wg-card:hover .wg-media,
.wg-card:focus-visible .wg-media { transform: scale(1.02); }

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

/* ---------- the cover ----------
   The cover keeps its authored 1200x750 canvas, so at any column
   width it scales rather than crops and the product UI inside it
   stays whole. --field-base is the ground the bare cover files need;
   they paint none of their own. */
.wg-media {
  position: relative;
  isolation: isolate;
  margin: 0;
  aspect-ratio: 1200 / 750;
  border-radius: var(--radius-media);
  overflow: hidden;
  background: var(--field-base, var(--bg-raised));
  /* The inset rim the case card uses, and only that: at this size an
     outline would draw a hard line around every image. */
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--ink) 3.5%, transparent),
    var(--shadow-rest);
}
.wg-media iframe {
  position: absolute;
  left: 50%; top: 50%;
  display: block;
  width: 1200px; height: 750px;
  border: 0;
  background: transparent;
  transform-origin: center;
  /* work-grid.js writes the real scale; this is the no-JS fallback */
  transform: translate(-50%, -50%) scale(0.5);
}
/* A raster cover is NOT scaled the way an iframe is. The iframes carry a
   fixed 1200x750 layout that has to be transformed to fit, but an image
   only needs to fill the slot — and forcing one onto that canvas squashed
   the Shopify cover, whose artwork is 1402x1122. object-fit crops it to
   the slot instead, so the tile keeps the row's 8:5 and the artwork keeps
   its own proportions. */
.wg-media img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%; height: 100%;
  border: 0;
  object-fit: cover;
  object-position: center;
}
/* A cover is an artifact, not an app: the iframe must not swallow the
   pointer, or the link wrapping it never sees a hover — and the hover
   is now the only thing that reveals the CTA. */
.wg-media iframe { pointer-events: none; }

/* The covers are hard-locked to the light theme (they are real product
   UI), so under the dark theme the artwork stays light and everything
   around it follows the page.

   Scoped to the FRAME, not to the media box. The frame holds two things:
   the artwork and the button that sits on top of it. With the light theme
   stopping at the media box, the CTA read the page's dark tokens while
   standing on light artwork, so it painted itself background:var(--ink),
   which is #F2F1ED in the dark theme: a near-white pill on a near-white
   cover, measured at 242,241,237 against a 242,242,239 ground. The disc
   went the same way, a dark circle carrying the dark-on-light arrow, so the
   arrow vanished into it. The button was invisible in dark mode and the
   hover we hang off the cover had nothing to show.

   The rule is the light theme belongs to whatever stands ON the artwork,
   which is the frame. It stops there: .wg-body underneath, the title and
   the description, still follows the page. */
[data-theme="dark"] .wg-frame {
  --ink: #1F1E24; --ink-body: #5C5B64; --ink-2: #6F6E76; --ink-3: #8D8C96;
  --line: rgba(19,18,23,0.14); --line-strong: rgba(19,18,23,0.32);
  --rule: rgba(19,18,23,0.07);
  --bg: #F9F9FB; --bg-raised: #FFFFFF;
}
[data-theme="dark"] .wg-media { background: var(--field-base, #FFFFFF); }

/* ---------- the CTA: an object the cover hands you on hover ----------
   A size variant of .btn-primary, not a new button: same ink fill,
   same pill, same disc that swings from north-east to due east. Only
   the metrics change — 28px disc, 6/16 padding, 16px gap — which
   brings it from 50px tall to 40px. The label stays at --text-small:
   16px is the floor for functional text and a CTA is functional.

   It rests 12px low and invisible with no shadow, then rises into
   place, fades up and lifts off the cover as the pointer arrives.
   The opacity/translate pairing is the case rows' own — fast fade,
   longer travel — so the reveal reads as the same hand that moves
   the covers in. */
.wg-cta {
  position: absolute;
  left: var(--space-md); bottom: var(--space-md);
  z-index: 2;
  gap: var(--space-md);
  padding: 6px 6px 6px var(--space-md);
  opacity: 0;
  translate: 0 12px;
  /* Explicitly none, so the shadow has a value to travel FROM. A
     shadow transitioning from the keyword `none` snaps rather than
     fades, which is what made the lift arrive in one step. */
  box-shadow: 0 0 0 0 rgba(19,18,23,0);
  transition: opacity var(--dur-fast) var(--ease-out-expo),
              translate var(--dur-med) var(--ease-out-expo),
              box-shadow var(--dur-med) var(--ease-out-expo),
              background-color var(--dur-fast) var(--ease-out-expo);
}
.wg-cta::after {
  width: 28px; height: 28px;
  background-size: 15px 15px;
}
.wg-card:hover .wg-cta,
.wg-card:focus-visible .wg-cta {
  opacity: 1;
  translate: none;
  /* --shadow-rest-sm is the small-component tier of the page's one
     shadow: the same three layers, scaled. The button is a small
     object sitting on artwork, so it takes the small tier. */
  box-shadow: var(--shadow-rest-sm);
}
/* The cover arms the button, not just the button.
   redesign.css keeps the arrow tied to the button itself, because a
   card-wide trigger made the icon answer hovers that had nothing to do with
   it. The cover is not a card-wide trigger: the CTA lives inside the
   artwork, the artwork is the thing being offered, and a pointer resting on
   it is already reading this case. So the frame, and only the frame, hands
   the button its hover state. The title and description below the cover
   still do not, which is the distinction that rule was protecting.
   The coming-soon variant is excluded: its disc holds a lock, and a lock
   that swings on hover promises an opening that is not there. */
.wg-frame:hover .wg-cta:not(.wg-cta--soon),
.wg-card:focus-visible .wg-cta:not(.wg-cta--soon) {
  background: color-mix(in srgb, var(--ink) 86%, var(--bg));
}
.wg-frame:hover .wg-cta:not(.wg-cta--soon)::after,
.wg-card:focus-visible .wg-cta:not(.wg-cta--soon)::after { rotate: 0deg; }

/* Without a pointer there is no hover, so the CTA would never appear
   at all. On touch it simply ships visible. */
@media (hover: none) {
  .wg-cta { opacity: 1; translate: none; box-shadow: var(--shadow-rest-sm); }
}
/* The phone tier in redesign.css stretches every .btn to the full column so a
   page CTA is thumb-width. This one is not a page CTA — it is an object the
   cover hands you, positioned inside the artwork — and at 100% it became a
   black bar clear across the image. It keeps the size it was drawn at, on
   every screen. */
@media (max-width: 620px) {
  .wg-cta { width: auto; }
}

/* ---------- not written up yet: one coming-soon button, site-wide ----------
   This is .mini-cta--soon from styles/studio-cards.css, verbatim: light
   fill, ink label, a solid ink disc with a lock in it. There is only one
   coming-soon component and the studio grid is where it is authored — the
   work grid takes it rather than inventing a second reading of the same
   state. It deliberately does NOT wear .btn-primary, so the ink fill and
   the swinging arrow disc stay the mark of a tile you can actually open.

   Two states in this row now read apart at a glance: an ink button with an
   arrow means there is a case study behind it, a light button with a lock
   means there is not yet. The lock's colours are hard-coded for the same
   reason they are in the studio file — the covers are locked to the light
   theme, so a themed fill would drift against artwork that never does. */
.wg-cta--soon {
  background: #F9F9FB;
  color: #1F1E24;
  /* not a link forward, so it does not offer the pointer of one */
  cursor: default;
}
.wg-card:hover .wg-cta--soon { background: #FFFFFF; }
/* Drawn in full, not inherited: without .btn-primary there is no ::after
   rule with `content` to extend, so the disc has to generate itself. */
.wg-cta--soon::after {
  content: ""; flex: none;
  width: 28px; height: 28px;
  border-radius: 50%;
  background-color: #1F1E24;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23F9F9FB' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4.5' y='10.5' width='15' height='10' rx='2.5'/%3E%3Cpath d='M8.5 10.5V7.2a3.5 3.5 0 0 1 7 0v3.3'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: center;
  background-size: 15px 15px;
  /* A lock does not turn. The arrow rests at north-east because that is an
     invitation to go somewhere, which is the opposite of what this says. */
  rotate: none;
}

/* ---------- the copy, underneath ----------
   Page text, not panel text: no inset on the left, so the meta line,
   the title and the description all start on the cover's own left
   edge and the tile reads as one column. */
.wg-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding-top: var(--space-md);
}
/* ---- read time ----
   How long the case study takes, sitting after the date on the same line
   and in the same voice. It answers the question a long case study
   provokes before it is opened, which is "am I starting something".

   Its own span rather than more words inside p1-meta, for two reasons:
   the figure changes whenever a case study is edited and should be
   findable on its own, and it wants to be quieter than the employer and
   the dates, which are credentials where this is a hint.

   The separator is drawn, not typed, so a language swap that replaces
   text content cannot take it with them. Same reason the hero's about
   column draws its own. The gap tightens to 8px here because the ·
   already does the separating that 16px was doing alone.

   Figures come from the rendered word count of each case page's <main>
   at 225 wpm: project-one 1767 words, project-two 1971, project-three
   1179, project-five 1085. Re-measure them if a case study is rewritten;
   a stale read time is worse than none. */
.wg-body .project-meta { gap: var(--space-xs); }
.wg-body .read-time { color: var(--ink-3); }
.wg-body .read-time::before {
  content: "·"; margin-right: var(--space-xs); color: var(--ink-3);
}
/* NO measure cap. The column IS the measure here. A 52ch cap left the
   paragraph ending ~180px short of the cover's right edge, so the tile
   read as a wide picture with a narrow column of text parked under its
   left corner — two widths in one object. The cover sets the width and
   every line under it runs to the same edge.
   The trade is a ~78-character line at 1440px, past the comfortable
   band. It holds because these are three-line paragraphs, not prose:
   if the copy grows, the cap comes back before the column does. */
.wg-card .project-impact { max-width: none; margin-bottom: 0; }
/* One step under --text-h3 (20-24px), which is sized for a full-width row and
   reads oversized on a half-width tile — especially now that the titles are
   sentences rather than product names. This keeps the same clamp shape as the
   scale above it, one stop down: 18px on a phone, 20px on a desktop.
   The 4px gap under it was far too tight for a 20px line: the description ran
   straight into the title with no seam between the two voices. 12px separates
   them without opening the tile up into two blocks. */
.wg-card .project-title {
  font-size: clamp(1.125rem, 1.02rem + 0.45vw, 1.25rem);
  margin-bottom: var(--space-sm);
}

/* ---------- entrance ----------
   The tile arrives as ONE object, the way a quote card does: a short rise
   with the fade, cover and copy together. They used to move apart — the
   cover travelling 28px while the copy only faded in place — which read as
   two arrivals inside something meant to be a single thing. 14px is the
   quote card's own distance and --dur-med its own duration, so the work
   section and the endorsements now enter with the same hand.

   --lag counts STRAIGHT THROUGH the grid — 1, 2, 3, 4 — rather than
   restarting at each row. Reading order is what the eye follows, and
   the eye does not care that tiles three and four begin a new row: a
   per-row reset made tiles one and three arrive together, which read
   as two separate events instead of one pass down the section. At
   90ms a step the last tile is only 270ms behind the first, so the
   cascade never becomes a queue.

   The CTA is deliberately outside all of this. It is a hover
   affordance, so the entrance must not fade it in and then take it
   away again — which is why it hangs off .wg-frame rather than off
   .wg-media, the element the entrance actually moves. */
.wg-card.reveal {
  opacity: 0; translate: 0 14px;
  transition: opacity var(--dur-med) var(--ease-out-expo) calc(var(--lag, 0) * 90ms),
              translate var(--dur-med) var(--ease-out-expo) calc(var(--lag, 0) * 90ms);
}
.wg-card.reveal.is-visible { opacity: 1; translate: none; }
.work-grid > :nth-child(2) { --lag: 1; }
.work-grid > :nth-child(3) { --lag: 2; }
.work-grid > :nth-child(4) { --lag: 3; }
/* The grid holds five tiles. Without this the fifth fell back to --lag 0 and
   arrived in step with the first, which broke the single pass down the
   section that the counter exists to create. */
.work-grid > :nth-child(5) { --lag: 4; }
.work-grid > :nth-child(5) { --lag: 4; }

/* The scroll-linked path in redesign.css hands every .reveal a view()
   animation on the element itself. The tile hands it back the same way
   .project.reveal does, so the child transitions above stay in charge
   on every browser — and so nothing here can freeze mid-entrance the
   way a view() timeline does inside a scroll container. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Only the animation is handed back. It used to force opacity: 1 here
       too, which cancelled the tile's own fade the moment the entrance moved
       onto the tile itself — the rise would play against a card that was
       already fully opaque. The transition above owns opacity now. */
    .wg-card.reveal { animation-name: none; }
  }
}

/* ---------- responsive ----------
   One column below 900px. Two 400px covers side by side stop showing
   product UI and start showing texture. */
@media (max-width: 900px) {
  .work-grid { grid-template-columns: 1fr; row-gap: var(--space-2xl); }
  /* One column IS reading order, and each tile now fills the screen on
     its own, so it arrives when it is scrolled to. A stored-up delay
     would just make the tile you are already looking at hang back. */
  .work-grid > * { --lag: 0; }
}

/* Reduced motion keeps every state and drops every journey: the tiles
   are simply there, and the CTA still answers the pointer, it just
   does not travel to do it. */
@media (prefers-reduced-motion: reduce) {
  .wg-card.reveal .wg-media,
  .wg-card.reveal .wg-body { opacity: 1; translate: none; transition: none; }
  .wg-cta { translate: none; transition: opacity var(--dur-fast) linear; }
  .wg-card:hover .wg-cta, .wg-card:focus-visible .wg-cta { translate: none; }
}
