/* Projects, under the orchard's first screen. Built from Figma keyan-folio-v2, node 870:19773.
   One --project-width column in the middle of the card (619px in Figma; now a share of the window, tokens.css). Each project: a logo, a title, two short paragraphs, a cover. */
.projects {
  display: flex;
  flex-direction: column;
  gap: var(--project-gap);
  width: min(max(var(--project-width), var(--project-min)), 100% - 32px);
  margin: 0 auto;
  padding: 24px 0 120px;
  color: #111111;
}

.project {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.project h2,
.project p {
  margin: 0;
  font: inherit;
}

/* The logo, 44px (32px, then 12px more), with no ellipse under it (Keyan, 2026-10-14), 4px in from the side, as the text. Each logo is its
   project's app tile from Figma keyan-folio-v2 node 551:2026 (projects/<name>/badge.svg), corners in the SVG. */
.badge {
  position: relative;
  width: 44px;
  height: 44px;
  margin-inline: 4px;
}

.badge .icon {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.project .body {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.project .text {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding-inline: 4px;
}

.project h2 {
  font-size: var(--project-title);
  font-weight: 350;
  line-height: normal;
  letter-spacing: -0.24px;
}

/* The date is #a19898, darkening to --project-date-hover as the hover grey comes in (--on, project-open.js), so it still
   reads on the grey. With the grey off (--project-grey 0) it stays as it is. */
.project h2 span {
  --date: color-mix(in srgb, var(--project-date-hover) calc(var(--on, 0) * var(--project-grey, 1) * 100%), #a19898);
  color: var(--date);
  font-weight: 300;
}

/* Grey like the date, or the title's black: --slash-dim in tokens.css. */
.project h2 .slash {
  color: color-mix(in srgb, var(--date) calc(var(--slash-dim) * 100%), #111111);
}

/* --project-cols columns, each an equal share: two, --project-columns apart (none in Figma, where the text is 12px), or one,
   each paragraph --project-stack under the one before. */
.project .lines {
  display: grid;
  grid-template-columns: repeat(var(--project-cols), minmax(0, 1fr));
  gap: var(--project-stack) var(--project-columns);
}

/* No line past --project-chars characters. 0.41em is a character of Lausanne at 300: at 75 of them, the longest line in
   every paragraph on the site (about 9,700 characters, home and case studies) is 75, and the average 65. ch would not do:
   it is a zero's width, 0.6em here, so 75ch lets lines run to about 100. Half of a two-column row never gets that wide. */
.project .lines p {
  max-width: calc(var(--project-chars) * 0.41em);
  color: rgba(17, 17, 17, 0.9);
  font-size: var(--project-text);
  line-height: var(--project-leading);
  /* No word left alone on a last line. */
  text-wrap: pretty;
}

/* The cover: the meadow at 90%, the wordmark in #2b2b2b through its own mask, and a riso wash in soft light at 30%.
   Every place is a share of the 619 by 338 frame, so the cover scales on small screens. */
.cover {
  position: relative;
  aspect-ratio: 619 / 338;
  overflow: clip;
  border-radius: var(--cover-radius, 20px);
  background: #ffffff;
  isolation: isolate;
}

/* A 1px line over the art, inside the edge (Figma drew it 3px; Keyan asked for 1px on every image). */
.cover::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid #f4f4f4;
  border-radius: inherit;
  pointer-events: none;
}

.cover img {
  position: absolute;
  max-width: none;
  object-fit: cover;
  pointer-events: none;
}

/* 911.203 by 512.824 at -61, -129 */
.cover .meadow {
  top: -38.166%;
  left: -9.855%;
  width: 147.206%;
  height: 151.723%;
  opacity: 0.9;
}

/* 164 by 44.459 at 228, 147.271. Figma draws the wordmark from a PNG; this is Ramp's own vector (wordmark.svg), so it
   stays sharp at any size. contain keeps the vector's shape; Figma's box is a touch narrower, so it sits centred in it. */
.cover .wordmark {
  position: absolute;
  top: 43.571%;
  left: 36.834%;
  width: 26.494%;
  height: 13.154%;
  background: #2b2b2b;
  mask: url('projects/ramp/wordmark.svg') center / contain no-repeat;
}

/* 750 by 500 at -25, -149 */
.cover .wash {
  top: -44.083%;
  left: -4.039%;
  width: 121.163%;
  height: 147.929%;
  opacity: 0.3;
  mix-blend-mode: soft-light;
}

/* Roblox's cover is the case study's hero, zoomed in on the minimap's top left, as in Figma node 924:29819.
   It runs past the frame on every side, so a press never shows an edge. */
.cover .shot {
  top: -5.68%;
  left: -32.25%;
  width: 185.5%;
  height: 148.6%;
}

/* Roblox's covers: four shots in Guo Chen's Oak grid (guochen.design, measured 2026-10-13): a wide one, two side by
   side, then a wide one, --cover-gap apart every way (20px, as hers). A small one is about half as wide, so its corners
   are --cover-small of --cover-radius, and the two sizes read as one shape. Every shot fills its cover; the wide ones
   are the cover's own 619 by 338 shape, the small ones their own. */
.covers {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--cover-gap, 20px);
}

.covers > .cover {
  grid-column: 1 / -1;
}

.covers > .cover.small {
  grid-column: auto;
  aspect-ratio: 1160 / 764;
  border-radius: calc(var(--cover-radius, 20px) * var(--cover-small, 0.5));
}

/* RevisionDojo's screens sit two to a row at their own 1920 by 903 shape, and its mascot drawings run across at
   theirs, 2889 by 869. */
.covers > .cover.small.screen {
  aspect-ratio: 3840 / 1806;
}

.covers > .cover.strip {
  aspect-ratio: 5778 / 1738;
}

.cover .full {
  inset: 0;
  width: 100%;
  height: 100%;
}

/* RevisionDojo's cover keeps the column's width but takes the picture's own shape, 3432 by 1854, a touch wider than
   619 by 338, so none of the study plan is cut off. */
.cover.revisiondojo {
  aspect-ratio: 3432 / 1854;
}

/* Intact's cover in two layers (images/intact-hero-bg-new.png, images/intact-hero-new.png). It keeps the column's width
   but takes the wash's own shape, 1699 by 1035, so it stands taller than Ramp's and the wash shows whole. The phones
   (1404 by 903) sit at the same scale as the wash, centred: 82.6% wide, 8.7% in from the left, 6.4% down. */
.cover.intact {
  aspect-ratio: 1699 / 1035;
}

.cover .bg {
  inset: 0;
  width: 100%;
  height: 100%;
}

.cover .phones {
  top: 6.38%;
  left: 8.68%;
  width: 82.64%;
  height: auto;
}

/* Bump's cover is its case study hero in two layers (Figma keyan-folio-v2 1062:104080). The frame is 1832 by 1015, but
   the cover stands taller, at Intact's shape, so the wash shows above and below the phones (Keyan, 2026-10-13). The wash
   covers the box, losing a little at the sides; the phones layer, drawn at the frame's full size, keeps the full width,
   centred top to bottom. That leaves the layer stopping short of the box, so its see-through shadow fades to nothing
   over its top and bottom 5.5% (in the images, 2026-10-13); before, it ended in a straight line near the bottom. */
.cover.bump {
  aspect-ratio: 1699 / 1035;
}

.cover.bump .phones {
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Hover, press and open (project-open.js). The grey sits on the project's own edges, hidden, and --on (0 to 1, from
   project-open.js) fades it in and grows it out by --project-hover-spread. It reads the tokens live, so a slider shows at once.
   The grey is the project's first child and the badge and body after it are positioned too, so it draws behind them by
   order alone. No layer group of its own, so the flowers (field.js) can rise above the print layer (print-gl.js). */
.project {
  position: relative;
  cursor: pointer;
  --on: 0;
}

.project > .body {
  position: relative;
}

.project-wrap {
  position: absolute;
  inset: calc(var(--project-hover-spread) * var(--on) * -1);
  border-radius: calc(16px + (var(--project-hover-radius) - 16px) * var(--on));
  background: var(--project-hover-color);
  /* --project-grey: 0 turns the grey off; the hover, press and magnet still move as before. */
  opacity: calc(var(--on) * var(--project-grey, 1));
  pointer-events: none;
}

/* The title is the project's button, for the keyboard. A click anywhere on the project does the same. */
.project-title {
  all: unset;
  cursor: pointer;
}

/* A project still in progress opens nothing (project-open.js), so it keeps the plain arrow. */
.project[aria-disabled="true"],
.project[aria-disabled="true"] .project-title {
  cursor: default;
}

/* The ring goes on the grey around the whole project, in place of the title's own. */
.project-title:focus-visible {
  outline: none;
}

html:not(.pointing) .project:has(.project-title:focus-visible:not([data-quiet])) .project-wrap {
  outline: 1px solid var(--ink);
}

/* The cover's layers sit in one box, cropped in by --project-zoom inside a frame that stays put. A press leaves it be. */
.cover .art {
  position: absolute;
  inset: 0;
  isolation: isolate;
  background: #ffffff;
  scale: var(--project-zoom, 1);
}

/* A press (.pressed, project-open.js) takes the whole project down, the grey jelly with everything on it, as one key:
   quick on --project-press-duration and -curve, and letting go springs it back on --project-release-duration and
   -curve, which overshoots a touch, after --project-release-delay. The scale property leaves the transforms other
   scripts set on the parts alone. */
.project {
  --release: var(--project-release-duration, 300ms) cubic-bezier(var(--project-release-curve, 0.34, 1.56, 0.64, 1)) var(--project-release-delay, 0s);
  --press: var(--project-press-duration, 90ms) cubic-bezier(var(--project-press-curve, 0.2, 0.8, 0.2, 1));
  transition: scale var(--release), opacity var(--release);
}

.project.pressed {
  scale: var(--project-press-scale, 0.97);
  transition: scale var(--press), opacity var(--press);
}

/* The click can fade too, as --project-press-fade says (project-open.js sets data-press-fade as it goes down): the cover
   alone, or the whole project, to --project-press-opacity, going down and coming back with the scale. The release
   curve runs past 1, which opacity simply stops at. */
.project .cover {
  transition: opacity var(--release);
}

.project.pressed .cover {
  transition: opacity var(--press);
}

.project.pressed[data-press-fade="image"] .cover,
.project.pressed[data-press-fade="jelly"] {
  opacity: var(--project-press-opacity, 0.95);
}

@media (prefers-reduced-motion: reduce) {
  .project,
  .project.pressed,
  .project .cover {
    transition: none;
  }
}

/* The design covers desktop only. Below 900px the column fills the card, and the paragraphs stack. */
@media (max-width: 899px) {
  .projects {
    width: auto;
    margin-inline: 16px;
    gap: 80px;
    padding-bottom: 80px;
  }
}

@media (max-width: 560px) {
  .project .lines {
    grid-template-columns: minmax(0, 1fr);
  }
}
