/* ---------------------------------------------------------------
   Mridul Goyal — portfolio
   Figma: Yuzen profiles & features → "Portfolio/case studies" (823:1708)
   --------------------------------------------------------------- */

:root {
  /* Figma variables */
  --content-primary:   #222222;
  --content-secondary: #717171;
  --content-tertiary:  #919191;
  --surface-secondary: #f7f7f7;
  --border-primary:    #e8e8e8;

  /* page-specific values from the frame */
  --ink:            #323232;
  --ink-nav:        #2d2d2d;
  /* Both greys clear WCAG AA (4.5:1) on white. The frame's #bababa measured
     1.94:1 and #777 measured 4.48:1 — a hair under. Anything lighter than
     #767676 cannot pass on white at this text size. */
  --ink-muted:      #757575;   /* 4.61:1 — tab unselected; role line; quiet labels */
  --tab-on:         #000000;   /* tab, hover + selected (Figma specs #2d2d2d) */
  --ink-year:       #595959;   /* 7.00:1 — company names, social links */
  --rule:           #dbdbdb;

  --gutter:         300px;  /* header / nav */
  --gutter-work:    284px;  /* work grid — cards add their own 16px, so their
                               text still lands flush at 300 */
  /* Every card shape: the mobile cards, the D2 desktop cards, and D1's
     travelling pill, which is the same box drawn behind the card. */
  --card-radius:    8px;
  --panel-w:        676px;
  --panel-ar:       676 / 384;   /* unitless — aspect-ratio rejects lengths */

  --ease-spring: cubic-bezier(0.34, 1.30, 0.64, 1);
  --ease-out:    cubic-bezier(0.22, 0.61, 0.36, 1);

  /* The raised look, shared by the travelling highlight on desktop and the
     selected card on mobile — one definition so they cannot drift apart. */
  --glass-edge: #f4f4f6;
  --glass-fill: linear-gradient(#ffffff 0%, #fafafa 100%);
  --glass-shadow:
    #ffffff 0 2px 4px inset,
    rgba(255, 255, 255, 0.5) 0 -4px 4px inset,
    #ffffff 0 -4px 16px inset,
    rgba(111, 111, 117, 0.07) 0 4px 10px inset,
    rgba(53, 53, 79, 0.4) 0 4px 14px -10px,
    rgba(53, 53, 79, 0.1) 0 11px 28px -10px;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: #ffffff;
  color: var(--ink);
  font-family: "Satoshi", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { max-width: 100%; display: block; }


/* ---------------------------------------------------------------
   Header
   --------------------------------------------------------------- */

.header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;                              /* profile picture → name */
  /* 76 + the nav's own 24px of top padding = the 100px the frame puts
     between "Designer @ Yulu" and the tabs. */
  padding: 100px var(--gutter) 76px;
}

/* avatar_new.png is a square illustration sitting on its own disc — recoloured
   from the frame's pink to white, so it reads as the page background. It needs
   no cropping, unlike the photo it replaced; the circle here only clips the
   corners of the source PNG. */
:root { --avatar-size: 70px; }

.avatar {
  flex: none;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  overflow: hidden;
}

.avatar img { width: 100%; height: 100%; }

.identity { display: flex; flex-direction: column; gap: 8px; }

.name {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.role {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink-muted);
}

.role-link {
  color: inherit;
  text-decoration: underline;
  text-underline-position: from-font;
  transition: color 0.2s var(--ease-out);
}

.role-link:hover { color: var(--tab-on); }


/* ---------------------------------------------------------------
   Nav
   --------------------------------------------------------------- */

/* Tab underline to the top of the panel's first BOX is 36px: the nav's 12px
   bottom padding plus 24px here. On Work the box is the hover rectangle, so
   its text sits a further 16px in — the card's own padding. */
:root {
  --panel-top: 24px;
  --nav-h: 73px;        /* measured stuck height */
}

/* Sticks to the top once the header scrolls past. It needs an opaque
   background — the work list and preview pass underneath — and a z-index
   above the travelling pill (0) and the project links (1). */
.nav {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 54px;
  padding: 24px var(--gutter) 12px;
  background: #ffffff;
}

.nav-item {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 0 0 12px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  font: inherit;
  font-size: 16px;
  font-weight: 500;
  cursor: pointer;
  transition: color 0.2s var(--ease-out), border-color 0.2s var(--ease-out);

  /* unselected */
  color: var(--ink-muted);
}

/* hover */
.nav-item:hover { color: var(--tab-on); }

/* selected */
.nav-item[aria-selected="true"] {
  color: var(--tab-on);
  border-bottom-color: var(--rule);
}

.nav-item:focus-visible {
  outline: 2px solid var(--tab-on);
  outline-offset: 4px;
}


/* ---------------------------------------------------------------
   Panels
   --------------------------------------------------------------- */

/* .work sets display:grid, which would otherwise beat the hidden attribute. */
.panel[hidden] { display: none; }

.panel--soon {
  padding: var(--panel-top) var(--gutter-work) 200px;
}

.soon {
  margin: 0;
  font-size: 16px;
  font-weight: 400;
  color: var(--content-tertiary);
}


/* ---------------------------------------------------------------
   Visuals — masonry gallery
   --------------------------------------------------------------- */

/* The gallery gets its own, much narrower gutter than the rest of the site:
   the text columns want the 300px margin for line length, but the artwork
   wants width. This is the one place the two disagree. */
.visuals {
  padding: var(--panel-top) 100px 160px;
}

/* Grid rather than multicol, because one image spans two columns and
   column-span only offers "all". The masonry comes from script.js, which
   converts each image's height into a grid-row span: rows are 1px with no
   row gap, so a span of N is N pixels tall, and the 36px vertical rhythm
   comes from each image's own margin instead. */
.visuals-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  column-gap: 36px;
  row-gap: 0;
  grid-auto-rows: 1px;
  align-items: start;
  /* The two-column image can only begin on a row where both its columns are
     free, which strands a hole above it. `dense` lets later single-column
     images back-fill that hole. It reorders them visually, which is fine
     here — the gallery has no meaningful sequence. */
  grid-auto-flow: row dense;
}

.visuals-grid img {
  width: 100%;
  height: auto;
  margin-bottom: 36px;             /* the vertical half of the 36px rhythm */
}

/* The only size variation. Nothing is pinned to a column — `dense` packs each
   piece into the first hole it fits, so the wide ones break the grid up and
   the rest close in around them. */
.visuals-grid .is-wide { grid-column: span 2; }


/* ---------------------------------------------------------------
   Work grid
   --------------------------------------------------------------- */

.work {
  position: relative;                 /* .glass is positioned against this */
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
  align-items: start;
  gap: 24px;                          /* hover rectangle → thumbnail */
  /* Asymmetric on purpose: the left is pulled in by the cards' own 16px so
     their text lands at 300, but nothing offsets the right, so the thumbnail
     takes the full gutter and its edge lands at 300 too. */
  padding: var(--panel-top) var(--gutter) 160px var(--gutter-work);
}

.projects {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Gap between the hover rectangles, not between the text: each card adds
     16px of its own padding on either side, so text-to-text reads as 40px. */
  gap: 8px;
}

.project { display: flex; }

.project-link {
  position: relative;
  z-index: 1;                          /* sits above .glass */
  display: inline-flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  text-decoration: none;
  color: inherit;
  border-radius: var(--card-radius);
}

.project-link:focus-visible {
  outline: 2px solid var(--ink-nav);
  outline-offset: 4px;
}

.project-title {
  font-size: 16px;
  font-weight: 500;                    /* Satoshi Medium */
  color: var(--ink);
}

.project-org {
  font-size: 16px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink-year);
}

/* "Experimental work" — a note rather than a client, so it sits back from the
   company names. #777 measures 4.48:1 on white, a hair under the 4.5:1 AA
   line; it was asked for explicitly. #767676 is the lightest grey that passes. */
.project-org--note { color: #777777; }

/* Card blurb. Mobile only — on desktop the pinned thumbnail carries the
   weight and the list stays a list. */
.project-desc { display: none; }


/* ---------------------------------------------------------------
   Travelling highlight ("liquid glass")
   Geometry is written by script.js; everything here is skin + easing.
   --------------------------------------------------------------- */

.glass {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  border-radius: var(--card-radius);
  border: 1px solid var(--glass-edge);
  background: var(--glass-fill);
  box-shadow: var(--glass-shadow);
  transform-origin: 50% 50%;
  will-change: transform, width, height, opacity;
  transition:
    transform 0.42s var(--ease-spring),
    width     0.42s var(--ease-spring),
    height    0.42s var(--ease-spring),
    opacity   0.20s var(--ease-out);
}

/* First appearance: fade in where it lands, don't fly in from 0,0 */
.glass.is-placing { transition: none; }

.glass.is-visible { opacity: 1; }

/* Soft inner top-light, mirrors nelson's .liquid-glass-highlight */
.glass-sheen {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0.7;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.9) 0,
    rgba(255, 255, 255, 0) 28px
  );
}

@supports (color: color(display-p3 1 1 1)) {
  .glass {
    background: linear-gradient(color(display-p3 1 1 1) 0%, color(display-p3 0.979 0.979 0.981) 100%);
    border-color: color(display-p3 0.9554 0.9554 0.9646);
    box-shadow:
      color(display-p3 1 1 1) 0 2px 4px inset,
      color(display-p3 1 1 1 / 0.5) 0 -4px 4px inset,
      color(display-p3 1 1 1) 0 -4px 16px inset,
      color(display-p3 0.4338 0.4338 0.4566 / 0.07) 0 4px 10px inset,
      color(display-p3 0.2085 0.2085 0.3025 / 0.4) 0 4px 14px -10px,
      color(display-p3 0.2085 0.2085 0.3025 / 0.1) 0 11px 28px -10px;
  }
}


/* ---------------------------------------------------------------
   Pinned preview
   --------------------------------------------------------------- */

.preview {
  position: sticky;
  top: calc(var(--nav-h) + 24px);   /* clears the stuck nav */
  justify-self: start;
  width: 100%;
  max-width: var(--panel-w);
  aspect-ratio: var(--panel-ar);
}

.preview-panel {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  transform: scale(0.98);
  filter: blur(8px);
  pointer-events: none;
  will-change: opacity, transform, filter;
  transition:
    opacity   0.34s var(--ease-out),
    transform 0.34s var(--ease-out),
    filter    0.34s var(--ease-out);
}

.preview-panel.is-active {
  opacity: 1;
  transform: none;
  filter: blur(0);
}

.preview-panel img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  border: 1px solid #000000;
  border-radius: 12px;
}

.preview-soon {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  border-radius: 12px;                    /* matches the real previews */
  background: var(--surface-secondary);
  border: 1px solid var(--border-primary);
  color: var(--content-tertiary);
  font-size: 18px;
  font-weight: 500;
}


/* ---------------------------------------------------------------
   About
   --------------------------------------------------------------- */

.about {
  padding: var(--panel-top) var(--gutter) 160px;
}

.about-intro {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: stretch;            /* the photo card matches the bio's height */
  gap: 72px;
}

/* The frame stacks two same-sized cards: a solid black one nudged down and
   right, and the white photo card on top of it. */
/* Fixed portrait rectangle. align-self:start opts out of the grid's stretch,
   so the aspect-ratio below — not the row — decides the height. The bio is
   what stretches now. */
.photo {
  position: relative;
  align-self: start;
  flex: none;
  width: 308px;
  padding: 0 14px 14px 0;          /* room for the black card to show through */
}

.photo::after {
  content: "";
  position: absolute;
  inset: 14px 0 0 14px;
  border-radius: 7px;
  background: #000000;
}

/* The frame mirrors the shot. The flip has to sit on the window, not the
   image: the offsets below crop the source in its original orientation, so
   flipping the image first would frame the wrong half of the room. */
.photo-window {
  position: relative;
  z-index: 1;
  display: block;
  overflow: hidden;
  aspect-ratio: 290 / 426;
  border-radius: 7px;
  border: 9px solid #ffffff;
  outline: 1px solid #323840;
  background: var(--surface-secondary);
  transform: scaleX(-1);
}

/* Same trick as the old avatar: the shot is a wide room and the subject is a
   small part of it, so the image is oversized and offset rather than
   object-fit:cover'd. Framed head-to-knees rather than chest-up, so the
   window holds a taller slice of the source. All three values are
   percentages of the window, which now has a fixed shape, so the crop is
   stable at every width. */
.photo-window img {
  position: absolute;
  width: 326.6%;
  height: auto;
  left: -162.5%;
  top: -117.6%;
  max-width: none;
}

.bio {
  display: flex;
  flex-direction: column;
  gap: 24px;                      /* paragraph to paragraph */
}

.bio p {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.6;
  color: #000000;
}

/* margin-top:auto eats whatever slack the photo leaves, pinning the logos to
   the bottom of the card — and it absorbs it HERE rather than spreading it
   between the paragraphs, which space-between would do. When the text is tall
   enough to fill the column the auto margin resolves to 0 and .bio's own 24px
   gap still holds the two apart. */
.worked { display: flex; flex-direction: column; gap: 24px; margin-top: auto; }

/* Scoped to .bio to outrank `.bio p`, which would otherwise paint this black. */
.bio .worked-label {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink-muted);
}

.worked-list,
.socials {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 24px;
}

.worked-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.5;
  /* Same grey as the "Worked with:" label above — the row reads as one
     unit, and the logos carry the colour. */
  color: var(--ink-muted);
}

.worked-logo {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: cover;
}

/* ---------------------------------------------------------------
   About — contact
   --------------------------------------------------------------- */

.chit-chat {
  display: flex;
  align-items: center;
  gap: 40px;
  margin-top: 90px;
  padding: 64px 0;
}

/* Sized to the block beside it — "Let's chit-chat." down to the socials row:
     22.4  label     (16px x 1.4)
   + 32    gap
   + 32    email     (the 24px arrow icon sets this, plus 8px of padding)
   + 16    gap       (the 32px gap, less the socials' -16px pull)
   + 22.4  socials   (16px x 1.4)
   = 125
   Flexbox can't derive this on its own: an <img> contributes its intrinsic
   size to the row height, so stretching it would just measure itself. Re-run
   the sum if the type scale changes. */
/* blue05 is 720x768, not square like blue04 was. Height stays auto so the
   mascot keeps its proportions; the width attribute on the img reserves the
   right box before it loads. */
.chit-chat-blob {
  flex: none;
  width: 125px;
  height: auto;
}

.chit-chat-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
}

.chit-chat-label {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
  color: var(--ink-muted);
}

.email {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 16px;
  font-weight: 500;
  color: #000000;
  text-decoration: none;
}

.arrow {
  flex: none;
  width: 24px;
  height: 24px;
  display: block;
  transition: transform 0.32s var(--ease-spring);
}

.email:hover .arrow { transform: translate(3px, -3px); }

/* The address and the socials are one block, tighter than the 32px above. */
.socials { margin-top: -16px; }

.socials a {
  font-size: 16px;
  font-weight: 500;
  color: var(--ink-year);
  text-decoration: underline;
  text-underline-position: from-font;
  transition: color 0.2s var(--ease-out);
}

.socials a:hover { color: var(--tab-on); }


/* ---------------------------------------------------------------
   Responsive
   --------------------------------------------------------------- */

/* Below ~1300 a 300px gutter leaves the preview column too narrow to read —
   600px of margin plus the widest card eats the row. Step back to the
   previous pair so the thumbnail stays usable. */
@media (max-width: 1300px) {
  :root { --gutter: 200px; --gutter-work: 184px; }
  .visuals-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 1100px) {
  :root { --gutter: 48px; --gutter-work: 48px; --panel-top: 72px; }
  .nav { gap: 36px; }
  .header { padding-top: 64px; padding-bottom: 64px; }

  /* Hover has no meaning on a touch screen, so the pinned preview column and
     the travelling pill both go. In their place each project becomes a self
     contained card: thumbnail, title, blurb. script.js moves the very same
     <figure> out of #preview and into the card, so nothing loads twice. */
  .work { grid-template-columns: minmax(0, 1fr); padding-bottom: 96px; }
  .preview { display: none; }
  .glass { display: none; }

  .projects { gap: 16px; align-items: stretch; }
  .project { display: block; }

  /* Resting state: outline only. */
  .project-link {
    display: flex;
    gap: 12px;
    padding: 16px;
    background: #ffffff;
    border: 1px solid var(--border-primary);
    border-radius: var(--card-radius);
    transition:
      box-shadow    0.3s var(--ease-out),
      border-color  0.3s var(--ease-out),
      background    0.3s var(--ease-out);
  }

  /* Selected state: the same raised treatment the pill uses on desktop.
     script.js gives this to whichever card is nearest the middle of the
     screen, so scrolling walks the highlight down the list. */
  .project-link.is-current {
    border-color: var(--glass-edge);
    background: var(--glass-fill);
    box-shadow: var(--glass-shadow);
  }

  /* The relocated thumbnail. In #preview it is one of five absolutely
     positioned, cross-fading layers; here it is just a block at the top of
     the card, sized by the panel's own aspect ratio. */
  .project-link .preview-panel {
    position: static;
    width: 100%;
    aspect-ratio: var(--panel-ar);
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
    will-change: auto;
  }

  /* The client belongs to the desktop list; the blurb replaces it here. */
  .project-org { display: none; }

  .project-desc {
    display: block;
    font-size: 16px;
    font-weight: 400;
    line-height: 1.5;
    color: var(--ink-muted);
  }
  .visuals { padding: var(--panel-top) var(--gutter) 96px; }
  /* One column: the wide image has nothing to span, so reset it. */
  .visuals-grid { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
  .visuals-grid img { margin-bottom: 16px; }
  .visuals-grid .is-wide { grid-column: span 1; }

  /* About stacks: photo over bio, and the blob sits above the address. */
  .about { padding-top: 40px; padding-bottom: 96px; }
  .about-intro { grid-template-columns: minmax(0, 1fr); gap: 48px; }

  /* Stacked, the photo is narrower than the column it now has to itself.
     The 14px of black offset lives inside the box's own padding, so
     centring the box would leave the white card sitting 7px left; the
     margin gives that back. */
  .photo { justify-self: center; margin-right: -14px; }

  /* Stacked, there is no photo alongside to leave slack, so the auto margin
     would collapse to the 24px gap. Give the section break back explicitly. */
  .worked { margin-top: 24px; }
  .bio { gap: 48px; }
  /* The company row keeps its 24px at every width — it reads as one
     block, and 16px ran the names together once the diamonds went. */
  .socials { gap: 16px; }
  .chit-chat { margin-top: 56px; padding: 0; }
}

/* ---------------------------------------------------------------
   D2 — the desktop treatment. index.html puts .d2 on <body>, so this is
   what the plain URL renders; ?d=1 drops the class and gives back D1, the
   earlier design, which is kept only for comparison.

   D1 keeps the list quiet, naming the client under each project and letting
   the travelling pill and the pinned thumbnail do the work. D2 gives the
   blurb that slot instead — the client name goes for good, not on hover —
   and turns each row into a card. Hover then does two things only: the card
   lifts, and the thumbnail changes.

   Because nothing opens or closes, every card keeps its height and the list
   never shifts under the cursor.

   The pill is retired here: its raised look now belongs to the card itself,
   and one highlight travelling behind five bordered cards would only fight
   with them.
   --------------------------------------------------------------- */

@media (min-width: 1101px) {
  .d2 .glass { display: none; }

  /* Narrower page margins than D1 — but the extra room is meant to fall
     between the card and the thumbnail, not into either of them. The
     header and the tabs move with the work grid, or they would sit 100px
     inside a card whose border makes the real margin plain to see. */
  .d2 { --d2-gutter: 250px; }

  .d2 .header,
  .d2 .nav,
  .d2 .about {
    padding-left: var(--d2-gutter);
    padding-right: var(--d2-gutter);
  }

  /* Pulled left by the card's own frame — 16px of padding plus its 1px
     border — so it is the card's TEXT that lands on the gutter, reading in
     one line with the name and the tabs. The border then hangs 17px outside
     that line, which is the intended trade: the words are what the eye
     tracks, and the card is only a shadow behind them. */
  .d2 .work {
    padding-left: calc(var(--d2-gutter) - 17px);
    padding-right: var(--d2-gutter);
  }

  /* The thumbnail is given a width of its own rather than taking whatever
     the column has left, so the card stays put and the slack between the
     two shows up as gap. min() lets it shrink on a narrower desktop. */
  .d2 .preview {
    width: min(570px, 100%);
    justify-self: end;
  }

  /* The card holds a set width — without one a max-content column would
     stretch to the longest blurb and put every one of them on a single
     line, and nothing would wrap. 310px is the size the column settled at
     and is kept deliberately; script.js measures the widest heading and
     writes --d2-card-w, which only ever widens it, so a longer title than
     today's can never be cut off. */
  .d2 .project-link {
    width: max(310px, var(--d2-card-w, 0px));
    border: 1px solid transparent;
    border-radius: var(--card-radius);
    transition:
      background   0.25s var(--ease-out),
      box-shadow   0.25s var(--ease-out),
      border-color 0.25s var(--ease-out);
  }

  /* The whole of hover: lift the card. No outline is drawn — the shadow and
     the fill are enough to raise it, and a 1px edge on top of them only
     hardens something meant to read as soft. The transparent border stays
     so the geometry does not shift. The thumbnail swap is script.js, the
     same as in D1. */
  .d2 .project-link:hover,
  .d2 .project-link:focus-visible {
    background: var(--glass-fill);
    box-shadow: var(--glass-shadow);
  }

  /* The blurb takes the client's place outright. */
  .d2 .project-org { display: none; }

  .d2 .project-desc {
    display: block;
    font-size: 16px;
    font-weight: 400;
    line-height: 1.5;
    color: var(--ink-muted);
    transition: color 0.25s var(--ease-out);
  }

  /* Darkens with the lift — the same grey the client names use, so the
     hovered card's blurb reads a step closer to the title. */
  .d2 .project-link:hover .project-desc,
  .d2 .project-link:focus-visible .project-desc { color: var(--ink-year); }
}


/* ---------------------------------------------------------------
   D3 — the mobile card brought up to the desktop, reached with ?d=3.

   Where D2 keeps a list of headings beside one pinned thumbnail, D3 gives
   every project the card it already has on a phone: its own picture on top,
   the title and blurb underneath, nothing hidden until hover. Two across,
   so the width goes into the pictures rather than into one very long row.

   Hover still has something to do — the card lifts and its blurb darkens,
   the same treatment as D2 — but it no longer reveals anything, because
   there is nothing left to reveal.
   --------------------------------------------------------------- */

@media (min-width: 1101px) {
  .d3 { --d3-gutter: 300px; }

  .d3 .glass,
  .d3 .preview { display: none; }      /* script.js empties #preview anyway */

  .d3 { --d3-card-frame: 25px; }       /* the card's 24px padding + 1px border */

  .d3 .header,
  .d3 .nav,
  .d3 .about {
    padding-left: var(--d3-gutter);
    padding-right: var(--d3-gutter);
  }

  /* Pulled out on both sides by the card's own frame, so that the picture
     and the text under it land on the gutter rather than the card's box
     doing so. The box then hangs 25px outside that line — which costs
     nothing, because with no outline the only thing drawn out there is the
     hover shadow. */
  .d3 .work {
    padding-left: calc(var(--d3-gutter) - var(--d3-card-frame));
    padding-right: calc(var(--d3-gutter) - var(--d3-card-frame));
  }

  /* One column, since the pinned thumbnail that took the second one has
     moved into the cards. */
  .d3 .work {
    grid-template-columns: minmax(0, 1fr);
    padding-bottom: 160px;
  }

  /* One project per row. Two across fitted more on a screen, but halved the
     picture — and the picture is the work. */
  .d3 .projects {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 24px;
  }

  .d3 .project { display: block; }

  .d3 .project-link {
    display: flex;
    width: auto;                       /* the grid sizes it, not a measured
                                          heading as in D2 */
    gap: 0;                            /* the three parts space themselves —
                                          picture to title and title to blurb
                                          are not the same distance */
    padding: 24px;                     /* the card is much larger here than on
                                          a phone, and 16px read tight on it */
    background: #ffffff;
    /* No outline. The thumbnail already draws its own black edge, and a grey
       one just outside it read as a second frame around the same picture.
       The border stays at 1px transparent so hover shifts nothing. */
    border: 1px solid transparent;
    border-radius: 12px;               /* not the 8px the smaller cards use —
                                          a corner has to grow with its box */
    transition:
      box-shadow    0.3s var(--ease-out),
      border-color  0.3s var(--ease-out),
      background    0.3s var(--ease-out);
  }

  .d3 .project-link:hover,
  .d3 .project-link:focus-visible {
    background: var(--glass-fill);
    box-shadow: var(--glass-shadow);
  }

  /* The relocated thumbnail. In #preview it is one of five absolutely
     positioned, cross-fading layers; here it is a block at the top of the
     card, sized by the panel's own aspect ratio. */
  .d3 .project-link .preview-panel {
    position: static;
    width: 100%;
    aspect-ratio: var(--panel-ar);
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
    will-change: auto;
  }

  .d3 .project-org { display: none; }

  /* Bigger than the 16px the rest of the site sets. The card gives the
     picture most of its height, and at 16px the words under it read as a
     caption rather than as the title of the piece. The title sits further
     from the picture than the blurb sits from the title, so the two lines
     of text group together and read as one block. */
  .d3 .project-title {
    font-size: 18px;
    margin-top: 16px;
  }

  .d3 .project-desc {
    display: block;
    margin-top: 8px;
    font-size: 18px;
    font-weight: 400;
    line-height: 1.5;
    color: var(--ink-muted);
    transition: color 0.25s var(--ease-out);
  }

  .d3 .project-link:hover .project-desc,
  .d3 .project-link:focus-visible .project-desc { color: var(--ink-year); }
}


/* Phone. 48px of gutter is a lot of nothing on a 390px screen, so the page
   pulls in to 24px a side and the cards get the width back. */
@media (max-width: 768px) {
  :root { --gutter: 24px; --gutter-work: 24px; }
}

/* The contact row is only 432px wide side by side (125 blob + 40 gap + the
   text), so it has no business stacking at the same width as the work grid
   and the photo — it keeps its row until the gutters genuinely squeeze it. */
@media (max-width: 600px) {
  .chit-chat {
    flex-direction: column;
    align-items: flex-start;
    gap: 24px;
  }
  /* Above the text now rather than beside it, so it is sized to look right
     rather than to match anything. */
  .chit-chat-blob { width: 140px; }
}

@media (prefers-reduced-motion: reduce) {
  .glass,
  .preview-panel,
  .arrow,
  .nav-item {
    transition-duration: 0.01ms !important;
  }
  .preview-panel { filter: none; transform: none; }
}
