/* ============================================================
   COMPONENTS: button, seam, stitched link, work row, person,
   stat band, case dialog.

   SHAPE RULE (one system, no exceptions). The page itself is ruled and
   near-square; things that sit ON the page are soft:

     part of the page
       work imagery and panels ... --p-radius-3 (8px)
       everything else ........... no radius
     objects on the page
       the cinematic canvas ...... --radius-canvas (28px)
       controls you reach for .... --radius-pill (buttons, nav
                                   capsule, icon buttons, chip)
       people .................... a circle

   Nothing outside that list takes a radius. The arch is retained in
   tokens for the hidden masthead hero only.
   ============================================================ */

/* ── Button ────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--p-space-2);
  min-block-size: var(--btn-h);
  padding-inline: var(--btn-pad-x);
  border-radius: var(--btn-radius);
  font-family: var(--font-body);
  font-size: var(--t-body-sm);
  font-weight: 600;
  white-space: nowrap;
  transition: background-color var(--p-dur-1) var(--p-ease-out),
              border-color var(--p-dur-1) var(--p-ease-out),
              transform var(--p-dur-1) var(--p-ease-out);
}
/* Scale, not a one pixel drop. A drop reads as a key going down and is
   all but invisible under a mouse; a scale reads as the button being
   pressed, and it reads the same under a finger. */
.btn:active { transform: scale(0.97); }
.btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

.btn--primary {
  background: var(--btn-bg-primary);
  color: var(--btn-fg-primary);
}
.btn--primary:hover {
  background: color-mix(in srgb, var(--btn-bg-primary) 92%, #000);
}

.btn--ghost {
  background: var(--btn-bg-ghost);
  color: var(--btn-fg-ghost);
  border: var(--btn-border-ghost);
}
.btn--ghost:hover { border-color: var(--color-text); }

.btn--sm {
  min-block-size: var(--btn-h-sm);
  padding-inline: var(--p-space-4);
  font-size: var(--t-meta);
}

/* ── Stitched link, the text-button variant ───────────────── */
.link-stitch {
  position: relative;
  display: inline-block;
  /* Body, not mono. These are actions a reader takes, and mono made
     them read as code beside the text they belong to. */
  font-size: var(--t-body-sm);
  color: var(--color-text);
  padding-block: var(--p-space-2);
}
/* Grows the hit area to 44px without moving the stitched underline. */
.link-stitch::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block: -4px;
}
.link-stitch::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: var(--p-border-med);
  /* A run of stitches rather than a solid rule. */
  background-image: repeating-linear-gradient(
    to right,
    var(--color-accent) 0 var(--seam-run),
    transparent var(--seam-run) calc(var(--seam-run) * 2)
  );
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--p-dur-2) var(--p-ease-out);
}
.link-stitch:hover::after,
.link-stitch:focus-visible::after { transform: scaleX(1); }

/* ── Seam, the section divider ─────────────────────────────────
   Drawn by the seam's own view timeline, not by an observer. Where a
   seam is on the page is a continuous question, and an observer only
   answers it once, then has to be told to let go, and needs a timer
   behind it in case its first callback never arrives. The timeline
   answers the question every frame, costs no JavaScript, and lets the
   seam unpick itself when the reader scrolls back up.

   The box stays put and its pseudo-element does the drawing: the seam
   itself carries the layout, and scaling it would collapse it.

   The drawn state is the base state, so a browser without view
   timelines shows every seam finished rather than showing none. */
.seam {
  position: relative;
  block-size: var(--p-border-med);
}
.seam::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to right,
    var(--color-stitch) 0 var(--seam-run),
    transparent var(--seam-run) calc(var(--seam-run) * 2)
  );
  transform: scaleX(1);
  transform-origin: left center;
}
@supports (animation-timeline: view()) {
  @keyframes seam-draw {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
  /* The range is the middle third of the seam's passage up the screen,
     so it finishes well before it reaches the top and well after it
     clears the bottom edge. A seam is two pixels tall, so `cover` is
     effectively one viewport of scroll and these percentages read as
     fractions of the screen, not of the element. */
  .seam::after {
    animation: seam-draw linear both;
    animation-timeline: view();
    animation-range: cover 20% cover 55%;
  }
}
/* The global reduced-motion rule only shortens durations, and a
   scroll-driven animation has none, so the seam is stood down here. */
@media (prefers-reduced-motion: reduce) {
  .seam::after { animation: none; transform: scaleX(1); }
}

/* ── Work row ──────────────────────────────────────────────────
   Two variants off one component. The flagships carry a cover, the
   index rows are a hairline list whose covers only appear under the
   pointer, so the section stays quiet until it is being read.

   The cover sits BELOW its own title, not above it. A reader going down
   the list meets the name of the project first and then sees it, which
   is the order the row is actually read in. Placement is done with grid
   areas, so the markup keeps the image first and a screen reader skips
   straight past it: it is decorative and carries an empty alt. */
.work-list { display: flex; flex-direction: column; }

.work-row {
  position: relative;
  display: grid;
  gap: var(--p-space-4);
  padding-block: var(--row-pad-y);
  border-block-start: var(--p-border-hair) solid var(--color-line);
  color: inherit;
  grid-template-areas:
    "index"
    "title"
    "facts"
    "cta"
    "media";
}
.work-row__media { grid-area: media; }
.work-row__index { grid-area: index; }
.work-row__title { grid-area: title; }
.work-row__facts { grid-area: facts; }
.work-row__cta   { grid-area: cta; }

@media (min-width: 768px) {
  .work-row {
    grid-template-columns: auto 1fr auto;
    column-gap: var(--p-space-5);
    align-items: baseline;
  }
  .work-row--index {
    grid-template-areas:
      "index title cta"
      ".     facts .";
  }
  .work-row--flagship {
    grid-template-areas:
      "index title cta"
      ".     facts ."
      "media media media";
  }
  .work-row--flagship .work-row__media { margin-block-start: var(--p-space-5); }
}
/* The heading already opens the list, so the first row needs no rule
   above it; the remaining borders separate one project from the next. */
.work-list .work-row:first-child { border-block-start: 0; }
.work-list .work-row:last-child { border-block-end: var(--p-border-hair) solid var(--color-line); }

.work-row__index {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--color-text-mute);
  transition: color var(--p-dur-1) var(--p-ease-out);
}
.work-row__title {
  font-family: var(--font-display);
  font-size: var(--t-h4);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  font-weight: 600;
  text-wrap: balance;
}
.work-row__facts {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--color-text-mute);
}
.work-row__cta {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--color-text-accent);
  opacity: 0;
  transform: translateX(-6px);
  transition: opacity var(--p-dur-2) var(--p-ease-out),
              transform var(--p-dur-2) var(--p-ease-out);
}
/* The row's own affordance arrives with the pointer or the focus ring;
   the six of them shouting "View project" at rest was noise. */
.work-row:hover .work-row__cta,
.work-row:focus-visible .work-row__cta {
  opacity: 1;
  transform: none;
}
.work-row:hover .work-row__index,
.work-row:focus-visible .work-row__index { color: var(--color-text-accent); }

@media (hover: none) {
  .work-row__cta { opacity: 1; transform: none; }
}

.work-row__media {
  border-radius: var(--p-radius-3);
  overflow: hidden;
  background: var(--color-bg-surface);
}
.work-row__media img { inline-size: 100%; }

/* The needle running the row on hover and on keyboard focus alike. */
.work-row::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: var(--p-border-med);
  background-image: repeating-linear-gradient(
    to right,
    var(--color-accent) 0 var(--seam-run),
    transparent var(--seam-run) calc(var(--seam-run) * 2)
  );
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--p-dur-2) var(--p-ease-out);
}
.work-row:hover::after,
.work-row:focus-visible::after { transform: scaleX(1); }

/* Hover preview for index rows: pointer devices only. */
.work-preview {
  position: fixed;
  z-index: var(--p-z-sticky);
  inline-size: 320px;
  aspect-ratio: 8 / 5;
  border-radius: var(--p-radius-3);
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  /* Opens from the centre rather than fading. Prefixed for Safari,
     which still wants -webkit- on the mask longhands. */
  -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 70%, transparent 71%);
  mask-image: radial-gradient(circle at 50% 50%, #000 70%, transparent 71%);
  -webkit-mask-size: 0% 0%;
  mask-size: 0% 0%;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  transition: opacity var(--p-dur-2) var(--p-ease-out),
              -webkit-mask-size var(--p-dur-3) var(--p-ease-out),
              mask-size var(--p-dur-3) var(--p-ease-out);
}
.work-preview[data-open="true"] {
  opacity: 1;
  -webkit-mask-size: 140% 140%;
  mask-size: 140% 140%;
}
.work-preview img { inline-size: 100%; block-size: 100%; object-fit: cover; }
@media (hover: none), (prefers-reduced-motion: reduce) {
  .work-preview { display: none; }
}

.work-footer { margin-block-start: var(--p-space-7); }

/* ── Person (testimonial attribution) ──────────────────────────
   Role and relationship are two facts, so they are two elements.
   Running them together on one line needed a dash to hold them
   apart, and the dash was doing structural work that markup does
   better. */
.person {
  display: flex;
  align-items: flex-start;
  gap: var(--p-space-4);
  margin-block-start: var(--p-space-5);
}
.person__avatar {
  inline-size: var(--avatar-size);
  block-size: var(--avatar-size);
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--color-bg-surface);
}
.person__body { display: flex; flex-direction: column; align-items: flex-start; }
.person__name { font-weight: 600; }
.person__role {
  font-size: var(--t-body-sm);
  color: var(--color-text-mute);
}
.person__rel {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--color-text-mute);
  margin-block: var(--p-space-2) var(--p-space-1);
}

/* ── Stat band ─────────────────────────────────────────────────
   No rules top or bottom. The numbers carry themselves, and the
   seam below the section is already doing the dividing. */
.stat-band {
  display: grid;
  gap: var(--p-space-6);
  margin-block-start: var(--space-block-y);
}
/* The number is the headline, so it sits first; dt still precedes dd
   in the DOM for assistive tech. */
.stat {
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-start;
  gap: var(--p-space-2);
}
.stat__value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-d3);
  line-height: var(--lh-d3);
  letter-spacing: var(--ls-d3);
  font-optical-sizing: auto;
}
/* The third value is words, not a number, and it stays a step smaller
   so it holds one line. Setting all three at the display size was
   tried: it wrapped to two lines and read as the loudest thing in the
   band rather than the third of three. Client decision. */
.stat__value--text {
  font-size: var(--t-h4);
  line-height: var(--lh-h4);
}
.stat__label {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--color-text-mute);
}
@media (min-width: 768px) {
  .stat-band { grid-template-columns: repeat(3, 1fr); }
}

/* ── Case dialog ───────────────────────────────────────────── */
.case {
  padding: 0;
  border: 0;
  border-radius: var(--p-radius-3);
  background: var(--color-bg);
  color: var(--color-text);
  inline-size: min(1100px, 100vw);
  max-inline-size: min(1100px, 100vw);
  block-size: min(90dvh, 1000px);
  max-block-size: 90dvh;
  box-shadow: var(--shadow-modal);
  overflow: hidden;
  /* Flex column so the body fills whatever the header leaves:
     no magic number to fall out of step with the header's padding. */
  display: flex;
  flex-direction: column;
}
.case:not([open]) { display: none; }

/* Entry and exit. The only real window on the site, and it used to
   arrive in a single frame with its backdrop.

   `display` and `overlay` are discrete properties, so both need
   `allow-discrete` or the dialog would blink out of the top layer at
   the start of its exit instead of at the end of it. `@starting-style`
   supplies the state to come from, so no class has to be set by hand.

   Never scale(0): the panel arrives from just under its resting place
   at 0.98, which is small enough to read as arriving and large enough
   to still be the same object. The exit is half the length of the
   entry and uses the mirrored curve, so the window answers faster than
   it was asked to open. */
.case {
  opacity: 1;
  translate: 0 0;
  scale: 1;
  transition: opacity var(--p-dur-2) var(--p-ease-out),
              translate var(--p-dur-2) var(--p-ease-out),
              scale var(--p-dur-2) var(--p-ease-out),
              display var(--p-dur-2) allow-discrete,
              overlay var(--p-dur-2) allow-discrete;
}
.case:not([open]) {
  opacity: 0;
  translate: 0 8px;
  scale: 0.98;
  transition-duration: var(--p-dur-1);
  transition-timing-function: var(--p-ease-in);
}
@starting-style {
  .case[open] { opacity: 0; translate: 0 8px; scale: 0.98; }
}

.case::backdrop {
  background: rgb(0 0 0 / 0.6);
  opacity: 1;
  transition: opacity var(--p-dur-2) var(--p-ease-out),
              display var(--p-dur-2) allow-discrete,
              overlay var(--p-dur-2) allow-discrete;
}
.case:not([open])::backdrop { opacity: 0; }
@starting-style {
  .case[open]::backdrop { opacity: 0; }
}

.case__head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--p-space-4);
  padding: var(--p-space-3) var(--p-space-5);
  background: var(--color-bg);
  border-block-end: var(--p-border-hair) solid var(--color-line);
}
.case__title {
  font-size: var(--t-h5);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.case__head-actions {
  display: flex;
  align-items: center;
  gap: var(--p-space-2);
  flex-shrink: 0;
}

.case__body {
  flex: 1;
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--p-space-6) var(--p-space-5);
}
/* Description first, never buried under the gallery, and across the
   whole width of the dialog in one column.

   Client decision, taken twice. Two things were tried and rejected: a
   column capped at --measure, which left the right half of the dialog
   empty, and the same width split into two columns, which kept the line
   at about 53 characters. One column across 1052px runs at about 105,
   which is above the 75 that prose is usually held to. Noted, not
   argued: do not quietly cap this again. */
.case__intro { margin-block-end: var(--space-block-y); }
.case__description { margin-block-end: var(--p-space-5); }
.case__tags {
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
  color: var(--color-text-mute);
  margin-block-start: var(--p-space-4);
}
/* No gap between the frames: the gallery reads as one continuous run of
   the project rather than as a stack of separate pictures. The radius
   goes with the gap, because a rounded corner on a picture that touches
   the next one leaves a notch of background between them. The gallery
   keeps one radius, on its own outer corners.

   `display: block` and not flex: a flex column with no gap still leaves
   the inline gaps of the images alone, but block plus `display: block`
   on the img is the simplest way to kill the baseline space under each
   one, which would otherwise show as a hairline of background. */
.case__gallery {
  display: block;
  border-radius: var(--p-radius-3);
  overflow: hidden;
}
.case__gallery img {
  display: block;
  inline-size: 100%;
  background: var(--color-bg-surface);
}
.case__video {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--color-bg-surface);
}
.case__video iframe {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  border: 0;
}
/* Click-to-load facade: no third-party request until asked. */
.case__video-poster {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  gap: var(--p-space-3);
  inline-size: 100%;
  color: var(--color-text);
  font-family: var(--font-mono);
  font-size: var(--t-meta);
  letter-spacing: var(--ls-meta);
}

@media (max-width: 767px) {
  /* The stat band sits inside Experience, and it is sized off the same
     token as that section's own title. Taken down with it, otherwise
     the numbers would out-shout the heading they belong to. */
  .stat__value { font-size: 1.625rem; }
  /* An index row is four short lines; 32px above and below them left
     more air around the row than the row itself occupies. */
  .work-row { padding-block: var(--p-space-5); }
  .work-footer { margin-block-start: var(--p-space-6); }
  /* Full-screen sheet on phones. */
  .case {
    inline-size: 100vw;
    max-inline-size: 100vw;
    block-size: 100dvh;
    max-block-size: 100dvh;
    border-radius: 0;
  }
  .case__body { padding-inline: var(--grid-pad); }
}
