/* ============================================================================
   ARTICLE SYSTEM — v2
   Type and layout for long-form case-study pages. Scoped to .case, so a new
   page gets the whole system by adding that one class.

   Built as a small set of repeating blocks, because that is what the design
   is: a media row, a caption row under it, a centred opener, a column of
   prose. Everything on the page is one of them.

   Values measured from the FMLY-26 frame (node 6440-3995), not eyeballed.
   Loaded AFTER styles.css, which supplies @font-face, the reset and the
   shared tokens (--font-sans, --font-mono, --ease-out).
   ========================================================================== */

.case {
  /* ── Widths ────────────────────────────────────────────────────────────
     One media measure with a 24px gutter gives the 2- and 3-up rows. The
     The ladder is 1512 / 1392 / 1200 / 800 — steps of 120, 192 and 400,
     so every width and every inset lands on the 8px grid. The design had a
     1140 and a 1216 doing the same job 76px apart; they are one value now. */
  --measure: 1392px;                                          /* media       */
  --prose:    800px;                                          /* body + heads*/
  --caption:  916px;   /* a single caption under a full-width figure        */
  --gutter:    24px;
  --col-1:   var(--measure);
  --col-2:   calc((var(--measure) - var(--gutter)) / 2);      /* 684         */
  --col-3:   calc((var(--measure) - var(--gutter) * 2) / 3);  /* 448         */
  --w-inset: 1140px;    /* a figure pulled in from the measure            */
  --w-narrow: 1092px;   /* the findings grid: 3 x 348 + 2 x 24             */
  --w-clip:    874px;   /* a screen recording, at the frame's own size      */
  --w-clip-sm: 684px;   /* a near-square capture, which 874 makes enormous  */
  --w-bleed: var(--page-w);    /* both now live in styles.css, because the */
  /* A caption cell is inset inside its media column: 684 - 120 = 564.     */
  --cap-inset: 60px;
  --case-pad: var(--page-pad);  /* back link is shared with the CV page.    */

  /* ── Spacing ───────────────────────────────────────────────────────────
     Five steps; every gap on the page is one of them. --sp-5 separates
     blocks, --sp-6 is the extra air above a section opener. */
  --sp-1:   8px;    /* title to lead, paragraph to paragraph             */
  --sp-2:  12px;    /* label to its content, sub-heading to its copy      */
  --sp-3:  24px;    /* the gutter between columns                         */
  --sp-4:  32px;    /* a figure to the caption that belongs to it         */
  --sp-5:  98px;    /* between sections                                   */
  --sp-6:  40px;    /* the extra air above a section opener               */
  --sp-7:  64px;    /* a block that belongs to the one above it            */
  --sp-8:  32px;    /* an opener or the page header to its own copy        */

  /* ── One radius for every piece of media ─────────────────────────────
     Measured at the hero panel and at an ordinary media block: both come
     out at ~53px, so it genuinely is a single value. 48 on the grid —
     edge-detection on an anti-aliased curve reads a little high. Declared
     in styles.css so the home page uses the same corner. */

  /* ── Type ──────────────────────────────────────────────────────────────
     Five roles. Nothing on the page may use a size outside this list. */
  /* The frame is drawn at 48/32/24/24/16; every size here is that ladder
     taken down a fifth, because at full size the article read as oversized
     on a real screen. The ratios between them are unchanged. */
  --t-h1:    38px;   /* 600 — page title and every section opener  */
  --t-lead:  26px;   /* 400 — the line under a title                */
  --t-h3:    19px;   /* 500 — a sub-heading inside a section        */
  --t-body:  19px;   /* 400 — body copy and captions                */
  --t-label: 13px;   /* 400 — mono, uppercase: kickers and credits  */

  --lh-tight: 1.2;
  --lh-sub:   1.6;   /* 38.4px on the 24px sub-heading */
  --lh-body:  1.2;
  --track:  -0.01em; /* the -0.32px the frame carries at 32px */

  /* ── Colour ─────────────────────────────────────────────────────────── */
  --ink:    #2d2d39;
  --muted:  #656581;
  --hair:   #e2e2e9;
  --ground: #f9f9fb;
  --frame:  #f0f3f5;   /* the fill behind framed media and tiles */
  --panel:  #f6f2fd;
  --accent: #6910e7;      /* set per page on <body> */

  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  line-height: var(--lh-body);
}

/* ── Page shell ──────────────────────────────────────────────────────────
   The widest thing on the page is the bleed block, so the shell is sized to
   it and each block narrows itself from there. */
.case__inner {
  max-width: calc(var(--w-bleed) + var(--case-pad) * 2);
  margin: 0 auto;
  padding: 0 var(--case-pad);
}

/* ══ TYPE PRIMITIVES ══════════════════════════════════════════════════════
   Five roles, and the frame uses no size outside them. Body copy is set at
   the same 24px as a sub-heading and separated from it by weight and
   leading, which is what makes the page read as one column of voice rather
   than a hierarchy of sizes. The 85% is the frame's own: it is an opacity on
   the text layer, not a lighter grey, so it holds over any ground. */
.t-h1    { font-size: var(--t-h1);   font-weight: 600; line-height: var(--lh-tight);
           letter-spacing: var(--track); color: var(--ink); }
.t-lead  { font-size: var(--t-lead); font-weight: 400; line-height: var(--lh-tight);
           letter-spacing: var(--track); color: var(--ink); }
.t-h3    { font-size: var(--t-h3);   font-weight: 500; line-height: var(--lh-sub);
           letter-spacing: var(--track); color: var(--ink); }
.t-body  { font-size: var(--t-body); font-weight: 400; line-height: var(--lh-body);
           color: var(--ink); opacity: .85; }
.t-label { font-family: var(--font-mono); font-size: var(--t-label); font-weight: 400;
           line-height: 1.4; text-transform: uppercase; color: var(--muted); }

/* ══ BLOCKS ═══════════════════════════════════════════════════════════════
   Every block centres itself in the shell and sets its own width. They are
   separated by one step, so stacking any two always looks right. */
.case > .case__inner > * + * { margin-top: var(--sp-5); }
/* An opener belongs to the section it introduces, so what follows it sits
   closer than the next section does. Same for the page header. */
.case > .case__inner > .opener + *,
.case > .case__inner > .head   + * { margin-top: var(--sp-8); }
/* The frame groups blocks into sections and the markup is a flat list, so
   where a block belongs to the one above it rather than starting something
   new, it says so. 64 is the within-a-section step; 98 separates sections. */
.case > .case__inner > .is-grouped { margin-top: var(--sp-7); }
/* .duo generates no box, so the three rules above land on the wrapper and
   never reach the media row inside it, which then has nothing above it.
   The same steps, applied one level down. */
.case > .case__inner > * + .duo > .media { margin-top: var(--sp-5); }
.case > .case__inner > .opener + .duo > .media,
.case > .case__inner > .head   + .duo > .media { margin-top: var(--sp-8); }
.case > .case__inner > .duo.is-grouped > .media { margin-top: var(--sp-7); }

.head, .opener, .prose { max-width: var(--prose); margin-inline: auto; }

/* The page header: credits, title, subtitle. Left-aligned — the frame only
   centres the section openers, and the header reads as the start of the
   column rather than a title card. */
.head { padding-top: clamp(40px, 6vw, 72px); }
.head__meta  { display: flex; flex-wrap: wrap; align-items: center;
               gap: var(--sp-2); margin-bottom: var(--sp-1); }
.head__meta .sep { color: var(--hair); }
.head__title { margin: 0; }
.head__lead  { margin-top: var(--sp-1); }

/* A section opener: title and lead, with a little air above. Left-aligned,
   like the page header and the copy under it — a centred heading over a
   left-aligned paragraph left the column with two different edges. */
.opener { padding-top: var(--sp-6); }
.opener__kicker { margin-bottom: var(--sp-2); }
.opener__title  { margin: 0; }
.opener__lead   { margin-top: var(--sp-1); }

/* Body copy. Left-aligned under the centred openers, which is how the design
   sets it — the heading announces, the paragraph reads. */
.prose { color: var(--ink); opacity: .85; }
.prose p + p { margin-top: var(--sp-1); }

/* A sub-heading and its copy, inside a section. */
.sub { max-width: var(--prose); margin-inline: auto; }
.sub__title { margin: 0; }
.sub__body  { margin-top: var(--sp-2); }
.sub__body p + p { margin-top: var(--sp-1); }

/* ── Media ───────────────────────────────────────────────────────────────
   A row of one, two or three items. --w picks the width; the modifiers pick
   the column count. A caption row placed under it with the same modifier
   lines up with it exactly. */
.media {
  display: grid;
  gap: var(--gutter);
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  /* Stretch, so two panels in a row are the same height even when one holds
     a video that sets its own. */
  align-items: stretch;
}
/* Sit the row on its baseline instead, for a pair where one item is shorter
   and the gap under it would otherwise read as a mistake. */
.media--bottom { align-items: end; }
/* A media row and the caption row that belongs to it. The wrapper generates
   no box here, so the two grids stay direct children of the shell and the
   desktop layout is exactly as it was; on a phone it becomes the grid that
   puts each caption under its own picture. */
.duo { display: contents; }
.media--1 { grid-template-columns: minmax(0, 1fr); }
.media--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.media--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Three widths that step away from the measure, for emphasis or restraint. */
.media--inset { max-width: var(--w-inset); }
.media--clip  { max-width: var(--w-clip); }
.media--clip-sm { max-width: var(--w-clip-sm); }
.media--bleed { max-width: var(--w-bleed); }

/* One item. An <img> and a <video> are interchangeable here — same box, same
   radius — so a still can be swapped for a clip without touching the layout. */
.figure { margin: 0; }
.figure > img,
.figure > video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  /* No placeholder fill here: every <img> carries width/height, so the box is
     already reserved, and a fill shows through transparent artwork — which is
     what put a lilac ground behind the nav diagram. */
}
/* Artwork that is a tight crop of a board, or already carries a rounded card
   of its own: the shared corner either cuts into the UI or fights the corner
   that is already drawn in the artwork. */
.figure--sharp > img,
.figure--sharp > video { border-radius: 0; }
/* A screen recording or shot presented inside a tinted panel, the way the
   design frames the nav: the media sits small and centred with its own
   smaller corner, and the panel carries the shared radius. Measured from
   the frame — media 30% of the panel, 8.5% of its height as padding. */
.figure--framed {
  --framed-w: 30%;
  /* Square by default: a screen recording already carries whatever corners
     the captured UI has, and rounding it again reads as a second frame. */
  --framed-radius: 0;
  --framed-shadow: 0 20px 44px -22px rgba(45, 45, 57, .15);
  display: grid;
  place-items: center;
  background: var(--frame);
  border-radius: var(--radius);
  padding: 8.5% 0;
}
.figure--framed > img,
.figure--framed > video,
.figure--framed > .crop {
  width: var(--framed-w);
  height: auto;
  border-radius: var(--framed-radius);
  box-shadow: var(--framed-shadow);
}
/* Crops the foot off a recording — a screen capture often carries a browser
   status bar that flickers in and out down there. Done here rather than by
   re-encoding, so nothing is re-compressed. --crop-h is the kept height in
   the source's own pixels. */
.crop {
  display: block;
  overflow: hidden;
  aspect-ratio: var(--crop-w) / var(--crop-h);
  line-height: 0;
  /* The clip inside is sized to overflow this box, so the shared corner has
     to be drawn here rather than on the video. */
  border-radius: var(--radius);
}
.crop > video,
.crop > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  border-radius: 0;
  box-shadow: none;
}
/* Transparent artwork rather than a screenshot: it has no chrome of its own,
   so it sits wider and takes neither an inner corner nor a shadow. */
.figure--art {
  --framed-w: 88%;
  --framed-radius: 0;
  --framed-shadow: none;
}

/* ── Tile ────────────────────────────────────────────────────────────────
   Media that fills its box rather than sitting on a panel — a photograph
   cropped to the frame. The research row is three of these. 448 x 416 is
   the third-column width against a height on the 8px grid. */
.tile {
  position: relative;
  aspect-ratio: 448 / 416;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--frame);
}
.tile > img,
.tile > video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
}

/* A label floating on media: a place, a prototype number. Padding and gap
   come off the spacing scale, so it sits at 51px tall against the 19px
   body size — the 53 the design draws, on the grid. */
.pill {
  position: absolute;
  left: 50%;
  bottom: var(--sp-4);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3);
  background: #fff;
  border-radius: 999px;
  font-size: var(--t-body);
  line-height: 1;
  color: var(--ink);
  white-space: nowrap;
  box-shadow: 0 var(--sp-1) var(--sp-3) calc(var(--sp-1) * -1) rgba(45, 45, 57, .30);
}

/* ── Rotator ─────────────────────────────────────────────────────────────
   Stacked media crossfading on a loop. Each child holds one slot of the
   cycle; the negative delays start them all at once at different phases
   rather than each waiting for its first turn. Set --rot-n to the number
   of children and the delays divide themselves. */
.rotator {
  position: relative;
  aspect-ratio: 448 / 416;
  --rot: 16s;
  --rot-n: 4;
}
.rotator > * {
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: rotate-media var(--rot) linear infinite;
}
.rotator > *:nth-child(2) { animation-delay: calc(var(--rot) / var(--rot-n) * -1); }
.rotator > *:nth-child(3) { animation-delay: calc(var(--rot) / var(--rot-n) * -2); }
.rotator > *:nth-child(4) { animation-delay: calc(var(--rot) / var(--rot-n) * -3); }
.rotator > *:nth-child(5) { animation-delay: calc(var(--rot) / var(--rot-n) * -4); }
@keyframes rotate-media {
  0%, 22%  { opacity: 1 }
  27%, 95% { opacity: 0 }
  100%     { opacity: 1 }
}
@media (prefers-reduced-motion: reduce) {
  .rotator > * { animation: none; opacity: 0; }
  .rotator > *:first-child { opacity: 1; }
}

/* A tile still waiting on its export. */
.tile__todo {
  position: absolute;
  inset: var(--sp-4);
  display: grid;
  place-items: center;
  text-align: center;
  font-size: var(--t-body);
  white-space: normal;
}

/* Media that brings its own rounded artwork should not be rounded twice. rounded artwork should not be rounded twice. */
.figure--square > img,
.figure--square > video { border-radius: 0; }
/* The three live components paint their own ground, so the radius belongs on
   the container that clips them, not on the artwork inside. */
.case .famly-loop,
.case .deck,
.case .profile-demo { border-radius: var(--radius); overflow: hidden; }
/* The loop's mark is clamped in styles.css; keep `.figure > img`'s
   width:100% from overriding that. */
.case .famly-loop > img { width: min(84%, 11.6% + 284px, 460px); height: auto; }

/* ── Caption row ─────────────────────────────────────────────────────────
   Takes the same column modifier as the media above it, so captions sit
   under their own item, 32px below it.

   Captions never run the full width of the media. In a multi-column row
   each cell is inset 60px, which is what turns a 684 column into the
   frame's 564 of text; a single caption narrows to its own measure instead,
   because one sentence set across 1392px stops being a caption. */
.captions {
  display: grid;
  gap: var(--gutter);
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  margin-top: var(--sp-4);
}
.captions--1 { grid-template-columns: minmax(0, 1fr); max-width: var(--caption); }
.captions--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.captions--3 { grid-template-columns: repeat(3, minmax(0, 1fr));
               max-width: var(--w-narrow); }
.captions--inset { max-width: var(--w-inset); }
.captions--clip  { max-width: var(--w-clip); }
.captions--clip-sm { max-width: var(--w-clip-sm); }
.captions--bleed { max-width: var(--w-bleed); }
/* The inset is on the cell, not the text, so the two columns of a 2-up stay
   the same width whatever their copy does. A 1-up and the findings grid are
   already narrowed by the row itself. */
.captions--2 > .caption { padding-inline: var(--cap-inset); }

.caption__title { margin: 0 0 var(--sp-2); }
.caption p { margin: 0; }
.caption p + p { margin-top: var(--sp-1); }
/* A caption row directly under media belongs to it: pull the gap in. */
.media + .captions { margin-top: var(--sp-4); }

/* ── Findings ────────────────────────────────────────────────────────────
   A mono label over a 3-up of sub-heading and copy, reading under the media
   it describes rather than announcing it. 1092 is the three 348 columns and
   their two gutters; the label sits 12px above them. */
.findings {
  width: 100%;
  max-width: var(--w-narrow);
  margin-inline: auto;
}
/* Under a bleed figure: the frame sets this row to the picture's width less
   its two corners, so the copy starts where the straight edge does. */
.findings--bleed {
  max-width: var(--w-bleed);
  width: calc(100% - var(--radius) * 2);
}
.findings__label { margin: 0 0 var(--sp-2); }
/* The frame sets this copy a step back from the sub-headings above it. */
.findings .t-body { color: var(--muted); }
.findings > .captions { margin-top: 0; }
/* the three columns inherit the row's width rather than their own cap */
.findings--bleed > .captions { max-width: none; }
/* It belongs to the media above it, like any caption row. */
.media + .findings { margin-top: var(--sp-4); }

/* ── Pair: media beside its text ─────────────────────────────────────────
   The design's other recurring shape — one item with its copy alongside,
   bottom-aligned, at the wide measure. */
.pair {
  display: grid;
  /* 2:1. The design had 0.84 here and 0.47 there; one ratio. */
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--sp-4);
  width: 100%;
  max-width: var(--w-inset);
  margin-inline: auto;
  align-items: end;
}
.pair > .figure { margin: 0; }
/* A tall screenshot with a paragraph beside it, at even columns and a
   narrower measure than the rest: 464 each keeps the copy off a 78-character
   line without letting the picture dominate the section. */
.pair--text {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  max-width: 960px;
}

/* ── Numbered list, from → to table, statement, rule ─────────────────────── */
.points { list-style: none; counter-reset: pt; display: grid; gap: var(--sp-2);
          max-width: var(--prose); margin-inline: auto; }
.points li { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2);
             align-items: baseline; padding-bottom: var(--sp-2);
             border-bottom: 1px solid var(--hair); color: var(--ink); }
.points li::before { content: counter(pt, decimal-leading-zero); counter-increment: pt;
                     font-family: var(--font-mono); font-size: var(--t-label); color: var(--accent); }

.shift { list-style: none; max-width: var(--prose); margin-inline: auto; }
.shift li { display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--sp-2);
            align-items: baseline; padding: var(--sp-2) 0;
            border-bottom: 1px solid var(--hair); font-size: var(--t-body); }
.shift li:first-child { border-top: 1px solid var(--hair); }
.shift b  { font-weight: 400; color: var(--muted); }
.shift i  { font-style: normal; color: var(--ink); }
.shift em { font-style: normal; font-family: var(--font-mono); color: var(--accent); }

.statement { max-width: var(--prose); margin-inline: auto; font-size: var(--t-lead);
             line-height: var(--lh-tight); letter-spacing: var(--track); color: var(--ink); }

.rule { border: 0; border-top: 1px solid var(--hair); max-width: var(--measure);
        margin-inline: auto; }

/* Numbers still to supply. Deliberately conspicuous: a claim with a
   placeholder in it is worse than no claim. Grep for "todo". */
/* No nowrap: it kept short metrics on one line, but a sentence-length
   placeholder then could not wrap and pushed the page sideways. The accent
   and the dashed rule already mark these out. */
.todo { color: var(--accent); font-weight: 500;
        border-bottom: 1.5px dashed currentColor; }

/* ── Footer ───────────────────────────────────────────────────────────────── */
/* The shared back link (styles.css) hangs off the frame itself, and
   .case__inner has already applied that inset — so here it drops its own and
   lands on the same edge it occupies on the CV. */
.case__inner > .backlink { max-width: none; padding-inline: 0; }

.case-foot { max-width: var(--measure); margin-inline: auto;
             padding: var(--sp-4) 0 clamp(60px, 8vw, 110px);
             border-top: 1px solid var(--hair); display: flex; flex-wrap: wrap;
             gap: var(--sp-2) var(--sp-3); justify-content: space-between;
             align-items: baseline; font-size: var(--t-label); color: var(--muted); }

/* ── Narrow screens ──────────────────────────────────────────────────────
   The frame is drawn at 1512 only, so these are the one set of values not
   measured from it. The sizes step down on the frame's own ratios; the body
   leading opens from 1.2 to 1.35, because 1.2 is a line length's worth of
   air at 24px over 800 and visibly not at 18px over a phone column. */
@media (max-width: 900px) {
  .case {
    --t-h1: 27px; --t-lead: 20px; --t-h3: 16px; --t-body: 15px; --t-label: 12px;
    --lh-body: 1.35; --lh-sub: 1.4;
    --sp-4: 24px; --sp-5: 56px; --sp-6: 24px; --sp-7: 40px; --radius: 24px;
  }
  /* Two pictures then two captions reads as a list of four unrelated things
     once the columns stack, so the pairs interleave instead. Both grids go
     display:contents, which lifts the figures and the captions into .duo,
     where order puts each caption behind its own figure. */
  .duo { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); }
  .duo > .media, .duo > .captions { display: contents; }
  .duo > .media    > *:nth-child(1) { order: 1; }
  .duo > .captions > *:nth-child(1) { order: 2; }
  .duo > .media    > *:nth-child(2) { order: 3; margin-top: var(--sp-5); }
  .duo > .captions > *:nth-child(2) { order: 4; }
  .duo > .media    > *:nth-child(3) { order: 5; margin-top: var(--sp-5); }
  .duo > .captions > *:nth-child(3) { order: 6; }

  .media--2, .media--3,
  .captions--2, .captions--3,
  .pair { grid-template-columns: 1fr; }
  .pair { align-items: start; gap: var(--sp-3); }
  /* The inset and the narrowed rows are desktop proportions; a phone column
     is already the measure. */
  .captions--2 > .caption { padding-inline: 0; }
  .captions--1, .captions--3 { max-width: var(--measure); }
  .shift li { grid-template-columns: 1fr; gap: var(--sp-1); }
  .shift em { display: none; }
  .shift b::after { content: " ↓"; }
}

/* ── Screen deck ─────────────────────────────────────────────────────────
   A row of iPhones standing on a pale field. Three sit in view at the bleed
   measure and the rest are a scroll away; the arrows only appear on hover or
   keyboard focus, so at rest the field is just phones.

   Phone, gap and field all come from the Figma's Implementation frame: the
   device is 414.24 wide on a 1512 measure with 40 between, and the ground is
   that frame's light grey. Everything is in container units off the field's
   own width, so the proportions hold at any size.
   -------------------------------------------------------------------------- */
.deck {
  container-type: inline-size;
  position: relative;
  aspect-ratio: 1512 / 791;
  --deck-pad: 3.704cqw;   /* 56px at the 1512 measure */
  --deck-gap: 2.116cqw;   /* 32px — the frame's 40, scaled with the device */
  background:
    radial-gradient(62% 90% at 88% 48%, rgba(255, 255, 255, .85) 0%, rgba(255, 255, 255, 0) 70%),
    linear-gradient(106deg, #e6e6e7 0%, #efeff0 52%, #f7f7f8 100%);
}

.deck__track {
  position: absolute;
  inset: var(--deck-pad) 0;
  display: flex;
  gap: var(--deck-gap);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.deck__track::-webkit-scrollbar { display: none; }

/* Height is what is fixed and the width follows the device's own ratio. */
.deck__phone {
  position: relative;
  height: 100%;
  aspect-ratio: 640 / 1317;
  flex: 0 0 auto;
}
/* The devices take no hover state: they are screens to read, not controls. */

/* The inset is top and bottom only, so a device leaving the field is cut by
   the field's own edge and not by a padded box 56px inside it. The first and
   last carry their inset as a margin instead, which — unlike padding on a flex
   scroller — is reliably counted into the scroll width.

   The first device starts on the left edge of the prose above it. Both the
   prose and this field are centred in the same shell, so that edge is half
   the difference between the field and the measure — no constant to keep in
   step, at any width. Narrow enough and the prose fills the shell, where that
   would fall inside the field's own inset, so the inset is the floor. */
.deck__phone:first-child {
  margin-left: max(var(--deck-pad), calc((100% - var(--prose)) / 2));
}
.deck__phone:last-child { margin-right: var(--deck-pad); }

/* The screen sits behind the device, in the glass aperture measured off the
   mockup: x 27–612 of 640, y 23–1294 of 1317. The frame image has that
   aperture cut out of it, so it trims the screen's own rounded corners and
   nothing has to line up by eye. Two classes deep to beat
   `.figure > img { width: 100% }`. */
.deck .deck__screen {
  position: absolute;
  left:    4.219%;
  top:     1.746%;
  width:  91.563%;
  height: 96.583%;
  max-width: none;
  object-fit: cover;
  /* Fills the screen's transparent corners, so the field cannot show
     through them. The radius is the glass aperture's own: r=101 on the 640
     mockup, which is 65px once the device is drawn at 414 wide, i.e. 4.33%
     of the field. Without it the screen's square box juts out past the
     device silhouette and prints a white tab at each corner. */
  background: #fff;
  border-radius: 4.33cqw;
}
/* Sits exactly over the screen so anything drawn live — the splash's dots —
   can be placed in the screen's own coordinates. */
.deck .deck__overlay {
  position: absolute;
  left:    4.219%;
  top:     1.746%;
  width:  91.563%;
  height: 96.583%;
  pointer-events: none;
}
/* The two full stops after "Signing you in" are painted out of the export; these
   three take their place and cycle, so the screen reads as mid-sign-in.
   Positions are the erased dots' own: x 511, baseline 845 of the 786x1704 screen. */
.deck__dots {
  position: absolute;
  left: 65.013%;
  top:  49.40%;
  display: flex;
  /* Container units, not percentages: this box shrink-wraps its dots, so a
     percentage width on them would resolve against a width that depends on
     them and collapse to zero. */
  gap: 0.14cqw;      /* ~4.5px on the 786-wide screen, half a dot */
}
.deck__dots i {
  width: 0.21cqw;    /* ~6.6px there; top is set so the row keeps its baseline */
  aspect-ratio: 1;
  border-radius: 50%;
  background: #2d2d39;
  /* Each dot rests at nothing rather than at a low opacity, so the row is
     never three dots sitting there. The lit window is a little over half the
     cycle and the dots are a third apart, which leaves two showing at a time:
     one bright, one on its way out. */
  animation: deck-dot 1.9s ease-in-out infinite;
}
.deck__dots i:nth-child(2) { animation-delay: .633s; }
.deck__dots i:nth-child(3) { animation-delay: 1.267s; }
@keyframes deck-dot {
  0%, 100% { opacity: 0; }
  10%      { opacity: 1; }
  60%      { opacity: 0; }
}

.deck .deck__device {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  pointer-events: none;
}
/* No scroll-snap on the track on purpose. The strip is only about a device
   and a half longer than its viewport, so most snap points fall outside the
   scroll range; Chrome then resolves a programmatic scroll to one of those
   and throws the strip back to the start instead of stepping. The arrows do
   the aligning themselves, in exact one-device steps. */

.deck__nav {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  width:  max(44px, 3.7cqw);
  height: max(44px, 3.7cqw);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  /* Dark rather than white: the strip scrolls white app screens past the
     arrows, and a white disc on a white screen disappears however much
     shadow it carries. */
  background: #111115;
  color: #fff;
  cursor: pointer;
  box-shadow: 0 2px 14px rgba(29, 17, 51, .22);
  opacity: 0;
  transition: opacity .25s var(--ease-out), background-color .2s ease-out, scale .18s ease-out;
}
/* `translate` does the centring, so `scale` can be animated on its own without
   either one having to restate the other. */
.deck__nav:hover  { background: #2d2d39; transition-duration: .12s; }
.deck__nav:active { scale: .93; }
.deck__nav--prev { left:  var(--deck-pad); }
.deck__nav--next { right: var(--deck-pad); }
.deck__nav svg { width: 42%; height: 42%; }
/* At either end there is nothing to scroll to, so the arrow goes away
   rather than sitting there doing nothing. */
.deck__nav[disabled] { opacity: 0; pointer-events: none; }

@media (hover: hover) {
  .deck:hover .deck__nav:not([disabled]) { opacity: 1; }
}
/* Keyboard users never hover, so focus has to reveal it on its own. */
.deck__nav:focus-visible { opacity: 1; outline: 2px solid var(--ink); outline-offset: 3px; }
/* No hover to reveal them on touch, and the strip scrolls by finger anyway. */
@media (hover: none) { .deck__nav { display: none; } }

/* ── Profile demo ─────────────────────────────────────────────────────────
   The responsive child profile demonstrating itself: a pointer reaches in,
   works the expand control, and the panel opens and closes on a loop.

   The control is a real element rather than part of the artwork, so its hover
   wash, the pointer's press and the icon swap all run off one clock and cannot
   drift apart. The pointer swaps from an arrow to a hand as it arrives, the
   way a real cursor does over a button.

   Geometry is in frame coordinates (1112x430). The control centres on x=668
   when narrow and x=706 when wide — in the wide state it is the first of four
   buttons, to the left of mail, phone and more. Percentage left/top resolve
   against the frame; percentage translate would resolve against the element.

   Cycle, as a fraction of --pd-cycle:
     0–14%   narrow, pointer travelling in
     14–20%  pointer resting on the control
     20–26%  press, panel expands
     26–52%  wide
     52–56%  press, panel collapses
     56–88%  narrow
     88–100% pointer withdraws
   -------------------------------------------------------------------------- */
.profile-demo {
  border-radius: var(--radius);
  --pd-cycle: 9s;
  --pd-x-narrow: 60.07%;   /* 668 / 1112 */
  --pd-x-wide:   63.49%;   /* 706 / 1112 */
  position: relative;
  overflow: hidden;
  aspect-ratio: 1392 / 538;
  background: linear-gradient(104deg,
              rgba(139, 102, 193, .30) 7.5%,
              rgba(228, 130, 202, .47) 108.9%);
}

/* Both states stack on the same baseline so swapping never shifts the panel's
   top edge. Scoped to .profile-demo to outrank `.shot img { width: 100% }`. */
.profile-demo .pd__card {
  position: absolute;
  top: 10%;
  left: 50%;
  height: 90%;
  width: auto;
  max-width: none;
  transform: translateX(-50%);
  transform-origin: 50% 0;
  animation: var(--pd-cycle) var(--ease-out) infinite;
}
.profile-demo .pd__card--narrow { animation-name: pd-narrow; }
.profile-demo .pd__card--wide   { animation-name: pd-wide; opacity: 0; }

/* The panel scales out of the narrower form as it opens, so the swap reads as
   one object growing rather than two images trading places. */
@keyframes pd-narrow {
  0%, 20%   { opacity: 1; transform: translateX(-50%) scale(1) }
  26%, 52%  { opacity: 0; transform: translateX(-50%) scale(1.04) }
  58%, 100% { opacity: 1; transform: translateX(-50%) scale(1) }
}
@keyframes pd-wide {
  0%, 20%   { opacity: 0; transform: translateX(-50%) scale(.94) }
  26%, 52%  { opacity: 1; transform: translateX(-50%) scale(1) }
  58%, 100% { opacity: 0; transform: translateX(-50%) scale(.94) }
}

/* ── The expand / collapse control ──────────────────────────────────────── */
.pd__btn {
  position: absolute;
  top: 16.3%;
  left: var(--pd-x-narrow);
  width: 3.1%;
  aspect-ratio: 1;
  margin: -1.55% 0 0 -1.55%;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: #2d2d39;
  animation: pd-btn var(--pd-cycle) var(--ease-out) infinite;
}
.pd__btn svg {
  position: absolute;
  width: 56%;
  height: 56%;
  animation: var(--pd-cycle) var(--ease-out) infinite;
}
.pd__btn .pd-ico--out { animation-name: pd-ico-out; }
.pd__btn .pd-ico--in  { animation-name: pd-ico-in; opacity: 0; }
/* The wash the Figma shows under the pointer. Its own element so it can fade
   independently of the icon. */
.pd__btn::before {
  content: "";
  position: absolute;
  inset: -6%;
  border-radius: 50%;
  background: rgba(45, 45, 57, .10);
  opacity: 0;
  transform: scale(.75);
  animation: pd-wash var(--pd-cycle) var(--ease-out) infinite;
}

@keyframes pd-btn {
  0%, 20%   { left: var(--pd-x-narrow) }
  26%, 52%  { left: var(--pd-x-wide) }
  58%, 100% { left: var(--pd-x-narrow) }
}
@keyframes pd-ico-out { 0%, 21% { opacity: 1 } 25%, 53% { opacity: 0 } 57%, 100% { opacity: 1 } }
@keyframes pd-ico-in  { 0%, 21% { opacity: 0 } 25%, 53% { opacity: 1 } 57%, 100% { opacity: 0 } }
@keyframes pd-wash {
  0%, 11%   { opacity: 0; transform: scale(.75) }
  15%, 19%  { opacity: 1; transform: scale(1) }
  20%       { opacity: 1; transform: scale(.86) }   /* the press */
  22%, 51%  { opacity: 1; transform: scale(1) }
  52%       { opacity: 1; transform: scale(.86) }
  54%, 86%  { opacity: 1; transform: scale(1) }
  90%, 100% { opacity: 0; transform: scale(.75) }
}

/* ── The pointer ────────────────────────────────────────────────────────── */
.pd__cursor {
  position: absolute;
  top: 112%;
  left: 78%;
  width: 2.4%;
  opacity: 0;
  filter: drop-shadow(0 2px 3px rgba(20, 20, 30, .32));
  animation: pd-cursor var(--pd-cycle) var(--ease-out) infinite;
  pointer-events: none;
}
.pd__cursor svg { position: absolute; top: 0; left: 0; width: 100%; height: auto;
                  animation: var(--pd-cycle) steps(1, end) infinite; }
/* Arrow on the way in, hand once it is over the control — as a real pointer
   does. Stepped, because a cursor changes shape instantly. */
.pd__cursor .pd-cur--arrow { animation-name: pd-cur-arrow; }
.pd__cursor .pd-cur--hand  { animation-name: pd-cur-hand; opacity: 0; }
@keyframes pd-cur-arrow { 0%, 11% { opacity: 1 } 12%, 88% { opacity: 0 } 89%, 100% { opacity: 1 } }
@keyframes pd-cur-hand  { 0%, 11% { opacity: 0 } 12%, 88% { opacity: 1 } 89%, 100% { opacity: 0 } }

@keyframes pd-cursor {
  0%        { opacity: 0; left: 78%;   top: 112% }
  5%        { opacity: 1 }
  14%, 19%  { opacity: 1; left: 60.6%; top: 16.0% }
  20%       { opacity: 1; left: 60.6%; top: 14.6% }   /* press */
  22%       { opacity: 1; left: 60.6%; top: 16.0% }
  26%, 51%  { opacity: 1; left: 64.0%; top: 16.0% }
  52%       { opacity: 1; left: 64.0%; top: 14.6% }   /* press */
  54%       { opacity: 1; left: 64.0%; top: 16.0% }
  58%, 86%  { opacity: 1; left: 60.6%; top: 16.0% }
  100%      { opacity: 0; left: 80%;   top: 112% }
}

@media (hover: hover) {
  .profile-demo:hover .pd__card,
  .profile-demo:hover .pd__btn,
  .profile-demo:hover .pd__btn svg,
  .profile-demo:hover .pd__btn::before,
  .profile-demo:hover .pd__cursor,
  .profile-demo:hover .pd__cursor svg { animation-play-state: paused; }
}
@media (prefers-reduced-motion: reduce) {
  .pd__card, .pd__btn, .pd__btn svg, .pd__btn::before,
  .pd__cursor, .pd__cursor svg { animation: none; }
  .pd__card--wide, .pd__cursor { opacity: 0 }
  .pd__card--narrow { opacity: 1 }
}

/* ── Phases ───────────────────────────────────────────────────────────────
   Four phases with a black disc travelling between them, clockwise. The disc
   arriving is what makes a phase active — one clock, so nothing drifts.

   Three layers: the white discs sit in flow, the black disc rides above them,
   the labels above that. No z-index on .phase, or it would make a stacking
   context the label could not climb out of, and the label would fall behind
   the disc and vanish.

   Geometry is the Figma's, expressed against the tile so the whole diagram
   scales with it: circles 142 of a 448 tile, 16 apart across and 10 down.
   -------------------------------------------------------------------------- */
.phases {
  --cycle: 6s;
  container-type: inline-size;
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}
.phases__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, 31.7cqw);
  gap: 2.23cqw 3.57cqw;
}

.phase {
  width: 31.7cqw;
  height: 31.7cqw;
  border-radius: 50%;
  display: grid;
  place-items: center;
  text-align: center;
  background: #fff;
  font-size: clamp(12px, 3.57cqw, 16px);
  line-height: 1.2;
}
.phase > span {
  position: relative;
  z-index: 2;
  color: var(--ink);
  animation: var(--cycle) linear infinite;
}

/* Travel takes a sixteenth of the cycle rather than a third, which is what
   reads as snappy; the squash is 4%, enough to feel like weight and not
   enough to notice as an effect. */
.phases__disc {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  width: 31.7cqw;
  height: 31.7cqw;
  border-radius: 50%;
  background: #111115;
  animation: phase-seat var(--cycle) cubic-bezier(.72, 0, .24, 1) infinite;
}
@keyframes phase-seat {
  0%,  19% { transform: translate(0, 0) scale(1, 1); }
  22%      { transform: translate(17.63cqw, 0) scale(1.045, .965); }
  25%, 44% { transform: translate(35.27cqw, 0) scale(1, 1); }
  47%      { transform: translate(35.27cqw, 16.97cqw) scale(.965, 1.045); }
  50%, 69% { transform: translate(35.27cqw, 33.93cqw) scale(1, 1); }
  72%      { transform: translate(17.63cqw, 33.93cqw) scale(1.045, .965); }
  75%, 94% { transform: translate(0, 33.93cqw) scale(1, 1); }
  97%      { transform: translate(0, 16.97cqw) scale(.965, 1.045); }
  100%     { transform: translate(0, 0) scale(1, 1); }
}

/* Each label states its own window. The disc's dwell is not evenly spaced in
   time, so one shared curve with delays drifts out of register — and the
   disc's clockwise order is Research, Ideas, Validation, Prototypes, which is
   not DOM order, since the grid reads left to right by row. Named rather
   than :nth-child, which counts every sibling — the disc is the first of
   them, so positional selectors land one seat out. */
.phase--research  > span { animation-name: phase-label-1; }
.phase--ideas     > span { animation-name: phase-label-2; }
.phase--prototype > span { animation-name: phase-label-4; }
.phase--validate  > span { animation-name: phase-label-3; }
@keyframes phase-label-1 { 0%,21% { color: #fff } 24%,95% { color: var(--ink) } 98%,100% { color: #fff } }
@keyframes phase-label-2 { 0%,21% { color: var(--ink) } 24%,45% { color: #fff } 48%,100% { color: var(--ink) } }
@keyframes phase-label-3 { 0%,46% { color: var(--ink) } 49%,70% { color: #fff } 73%,100% { color: var(--ink) } }
@keyframes phase-label-4 { 0%,71% { color: var(--ink) } 74%,95% { color: #fff } 98%,100% { color: var(--ink) } }

@media (prefers-reduced-motion: reduce) {
  .phases__disc, .phase > span { animation: none; }
  .phase--research > span { color: #fff; }
}

@media (prefers-reduced-motion: reduce) {
  .deck__dots i { animation: none; opacity: .6; }
}
