/* ============================================================
   Vignesh Vijayan — portfolio v3
   Built from the Paper file "Scratchpad", page 1.
   Three modes share one skeleton; only surface, ink and the
   display face change.
   ============================================================ */

/* ── Fonts ─────────────────────────────────────────────── */

@font-face {
  font-family: 'Open Sauce Two';
  src: url('../fonts/open-sauce-two-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Open Sauce Two';
  src: url('../fonts/open-sauce-two-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Open Sauce Two';
  src: url('../fonts/open-sauce-two-600.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Open Sauce Two';
  src: url('../fonts/open-sauce-two-800.woff2') format('woff2');
  font-weight: 800; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Zalando Sans Expanded';
  src: url('../fonts/zalando-sans-expanded-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
/* Commercial face (Gracetype Studios) — needs a webfont licence before launch. */
@font-face {
  font-family: 'Rustic Wave';
  src: url('../fonts/rustic-wave.otf') format('opentype');
  font-weight: 400; font-style: normal; font-display: swap;
}

/* ── Tokens ────────────────────────────────────────────── */

:root {
  --ui: 'Open Sauce Two', system-ui, -apple-system, sans-serif;

  --pad-x: clamp(16px, 6.25vw, 90px);
  --pad-y: clamp(16px, 9.27vh, 86px);
  --gutter: clamp(18px, 1.7vw, 24px);
  --radius-card: 8px;

  /* Width alone is the wrong input on a scaled Windows display: a 1920x1080
     laptop at 150% reports a 1280x720 viewport, so the type kept its width-
     derived size while losing a third of the height it had to breathe in.
     Taking the smaller of the two keeps the title the same fraction of the
     card at 1280x720 as it is at 1440x900. 8vh is tuned so the Mac is
     unchanged: at 900px tall it resolves to the same 72px cap. */
  --title-size: clamp(32px, min(5vw, 8vh), 72px);
  --title-top: clamp(72px, 14.01vh, 130px);
  --list-top: clamp(64px, 11.64vh, 108px);

  --portrait: #D9D9D9;
  --ease: cubic-bezier(.22, 1, .36, 1);

  /* Driven by js/app.js */
  --hero-o: 1;
  --hero-y: 0px;
  --proj-o: 0;
  --proj-y: 6vh;
  --list-y: 0px;
}

:root[data-mode="1"] {
  --page: #000A0F;
  --card: #FFFFFF;
  --ink: #1A1A1A;
  --ink-dim: #929292;
  --surface: #F5F5F5;
  --chip: #F6F6F6;
  --chip-ink: #565656;
  --hairline: rgba(26, 26, 26, .10);
  --swatch: #C4730B;
  --display: 'Open Sauce Two', system-ui, sans-serif;
  --display-weight: 800;
  --display-track: -.0417em;
  --title-measure: 13em;
}

:root[data-mode="2"] {
  --page: #000000;
  --card: #1D2430;
  --ink: #FFFFFF;
  --ink-dim: #98A0AD;
  --surface: #262E3C;
  --chip: #262E3C;
  --chip-ink: #C2C9D4;
  --hairline: rgba(255, 255, 255, .14);
  --swatch: #00BFFF;
  --display: 'Rustic Wave', 'Open Sauce Two', cursive;
  --display-weight: 400;
  --display-track: -.0139em;
  --title-measure: 13em;
}

:root[data-mode="3"] {
  --page: #0A0000;
  --card: #C4CBC2;
  --ink: #1B1B1B;
  --ink-dim: #6C736A;
  --surface: #B9C0B7;
  --chip: #B9C0B7;
  --chip-ink: #474E46;
  --hairline: rgba(27, 27, 27, .12);
  --swatch: #E34B3C;
  --display: 'Zalando Sans Expanded', system-ui, sans-serif;
  --display-weight: 400;
  --display-track: -.0139em;
  --title-measure: 13em;
}

/* ── Base ──────────────────────────────────────────────── */

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

html {
  -webkit-text-size-adjust: 100%;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html::-webkit-scrollbar { width: 0; height: 0; }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--ui);
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background-color .5s var(--ease);
}

h1, h2, p, ul { margin: 0; }
ul { padding: 0; list-style: none; }

.shader {
  position: fixed;
  inset: 0;
  background: var(--page);
  transition: background-color .5s var(--ease);
}
.shader canvas { display: block; }

/* ── Card ──────────────────────────────────────────────── */

/* The edges are set individually rather than through `inset` so each one can
   be transitioned when the card expands into the case study. */
.card {
  position: fixed;
  top: var(--pad-y);
  right: var(--pad-x);
  bottom: var(--pad-y);
  left: var(--pad-x);
  border-radius: var(--radius-card);
  background: var(--card);
  overflow: clip;
  transition:
    background-color .5s var(--ease),
    top .55s var(--ease), right .55s var(--ease),
    bottom .55s var(--ease), left .55s var(--ease),
    border-radius .55s var(--ease),
    transform .7s var(--ease), opacity .45s var(--ease);
}

/* ── Intro ─────────────────────────────────────────────── */

/* A small square settles onto the gradient, holds for a beat, then pushes
   out to become the card. Driven entirely by animations rather than a class
   the way the earlier version was: `backwards` fill hides the from-state
   during each delay, so nothing depends on script running to reveal itself,
   and the 100% frames equal the resting CSS, so control hands back cleanly
   to the transitions that drive the case-study handoff.

   Timeline — square lands by .45s, holds to .95s, card open at 1.7s,
   contents in by ~2.4s. */

@keyframes card-land {
  from { opacity: 0; transform: scale(.72); }
  to   { opacity: 1; transform: none; }
}

@keyframes card-open {
  /* 56% of 1.7s is .95s: the square's whole life before it expands. */
  0%, 56% {
    top: calc(50% - 64px);
    right: calc(50% - 64px);
    bottom: calc(50% - 64px);
    left: calc(50% - 64px);
  }
  100% {
    top: var(--pad-y);
    right: var(--pad-x);
    bottom: var(--pad-y);
    left: var(--pad-x);
  }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

:root.intro .card {
  animation:
    card-land .45s var(--ease) backwards,
    card-open 1.7s var(--ease) backwards;
}

:root.intro .nav { animation: rise .55s var(--ease) 1.65s backwards; }
:root.intro .hero__title { animation: rise .55s var(--ease) 1.75s backwards; }
:root.intro .hero__foot { animation: rise .55s var(--ease) 1.85s backwards; }

/* ── Leaving for a case study ──────────────────────────── */

/* The plate that grows out of the clicked card. It starts at the card's own
   measured bounds and colour, and finishes filling the viewport in white —
   which is the case study's own ground, so the two pages meet on the same
   surface whichever mode the visitor was in. */
.page-open {
  position: fixed;
  z-index: 20;
  background: var(--card);
  transition:
    top .56s var(--ease), left .56s var(--ease),
    width .56s var(--ease), height .56s var(--ease),
    border-radius .56s var(--ease),
    background-color .56s var(--ease);
}
/* Geometry is driven inline by the script; this only carries the colour it
   settles on. */
.page-open--full { background: #FFFFFF; }


/* The card grows to fill the viewport and loses its corners, so the case
   study reads as the same white surface opened up rather than a new page. */
:root.leaving .card {
  top: 0; right: 0; bottom: 0; left: 0;
  border-radius: 0;
}
:root.leaving .shader { opacity: 0; transition: opacity .45s var(--ease); }
:root.leaving .nav,
:root.leaving .fold--projects,
:root.leaving .fold--hero {
  opacity: 0;
  transition: opacity .28s var(--ease);
}

/* ── Nav ───────────────────────────────────────────────── */

.nav {
  position: absolute;
  z-index: 2;
  top: var(--gutter);
  left: var(--gutter);
  right: var(--gutter);
  display: flex;
  align-items: center;
  gap: clamp(24px, 3.65vw, 52px);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -.5px;
  line-height: 1.1;
}

.nav__home {
  position: relative;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;   /* UA styles reset this on form controls */
  color: var(--ink);
  cursor: pointer;
  transition: opacity .35s var(--ease), visibility 0s linear 0s;
  text-align: left;
}
.nav__home-label { transition: opacity .35s var(--ease); }

/* The name belongs to the hero. On the projects fold it goes entirely —
   visibility, not just opacity, so the button leaves the tab order and the
   accessibility tree rather than lingering as an invisible control. The
   zero-length visibility transition is delayed so it waits for the fade. */
:root[data-fold="projects"] .nav__home {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .35s var(--ease), visibility 0s linear .35s;
}

/* The role, the mode switch and the clock all belong to the hero. */
.nav__role {
  color: var(--ink);
  transition: opacity .35s var(--ease);
}
:root[data-fold="projects"] .nav__role { opacity: 0; }

.nav__right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: clamp(16px, 1.8vw, 26px);
  transition: opacity .35s var(--ease);
}
:root[data-fold="projects"] .nav__right {
  opacity: 0;
  pointer-events: none;
}

/* Mode switch */
/* One control rather than three: an orb that breathes in the current mode's
   colour, labelled so its job is not left to inference. Each press advances
   to the next mode and the orb swells once as it recolours.

   Built natively rather than pulled from a component library — the page has
   no bundler or framework runtime, and a soft-lit circle on a scale loop is
   a gradient and a keyframe. */
.switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  flex-shrink: 0;
  transition: opacity .35s var(--ease);
}

/* Painted by the thinking-orbs engine (see js/app.js); the element is just
   the surface it draws on. Sized here in css to match the tuned preset. */
.switch__orb {
  display: block;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

/* A short swell on press. The orb's own animation is drawn on the canvas,
   so this scales the whole surface rather than fighting it. */
.switch--pulse .switch__orb { animation: orb-pulse .5s var(--ease); }

@keyframes orb-pulse {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.22); }
  100% { transform: scale(1); }
}

.switch__label {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -.2px;
  line-height: 1;
  color: var(--ink);
  transition: color .5s var(--ease);
}

.switch:hover .switch__label { opacity: .7; }
.switch:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; border-radius: 4px; }

/* Clock */
/* Set back from the name and the switch by size alone — same ink as
   everything else in the nav. */
.clock {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: -.1px;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  transition: color .5s var(--ease);
}
.clock__icon { flex-shrink: 0; width: 14px; height: 14px; overflow: visible; fill: currentColor; }

/* ── Folds ─────────────────────────────────────────────── */

.fold {
  position: absolute;
  inset: 0;
  padding: 0 var(--gutter) var(--gutter);
  display: flex;
  flex-direction: column;
}
.fold--hero {
  padding-top: var(--title-top);
  opacity: var(--hero-o);
  transform: translateY(var(--hero-y));
}
.fold--projects {
  padding-top: var(--list-top);
  opacity: var(--proj-o);
  transform: translateY(var(--proj-y));
}
:root[data-fold="hero"] .fold--projects,
:root[data-fold="projects"] .fold--hero { pointer-events: none; }

/* ── Hero ──────────────────────────────────────────────── */

.hero__title {
  max-width: var(--title-measure);
  font-family: var(--display);
  font-weight: var(--display-weight);
  font-size: var(--title-size);
  letter-spacing: var(--display-track);
  line-height: 1.1;
}

.hero__foot {
  margin-top: auto;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 32px;
}

.hero__cue {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -.2px;
  line-height: 1.5;
}

.hero__aside {
  display: flex;
  /* The bio sets the height; the photograph squares off against it. */
  align-items: stretch;
  gap: 26px;
}

.bio { display: flex; flex-direction: column; align-items: flex-start; gap: 25px; }
.bio__copy {
  width: 296px;
  max-width: 100%;
  /* This block sets the height of the whole bio row, photograph included, so
     it eases back a little on a short viewport rather than holding 14px while
     the card loses a third of its height. */
  font-size: clamp(13px, 1.56vh, 14px);
  font-weight: 500;
  letter-spacing: -.2px;
  line-height: 1.5;
}
.bio__link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -.2px;
  line-height: 1.5;
  color: var(--ink);
  text-decoration: none;
  box-shadow: inset 0 -1px 0 var(--hairline);
  transition: box-shadow .3s var(--ease);
}
.bio__link:hover { box-shadow: inset 0 -1px 0 currentColor; }

/* Two secondary links on one row. They wrap rather than shrink if the column
   ever gets narrower than the pair, so neither loses its underline. */
.bio__links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 22px;
}

/* Both links take a 16px icon in front of the label, on one shape and one
   stroke weight, so the pair reads as a set rather than as a link and a
   decorated link. Stroked in currentColor, so they recolour with the mode
   and dim with the text on hover. */
.bio__icon { flex-shrink: 0; }

.portrait {
  /* The bio sets the height (the row stretches); the width is written back
     from it by js/app.js so the frame keeps the photograph's own ratio.
     `aspect-ratio` cannot do this on its own — a flex item has no intrinsic
     main size to derive from, and the box collapses to nothing. The fallback
     below is the desktop size, so the frame is still right without script. */
  flex: 0 0 auto;
  align-self: stretch;
  width: var(--portrait-w, 172px);
  border-radius: 4px;
  /* The flat fill stays underneath as the ground the photo loads onto. The
     slot now takes its height from the bio beside it, so `cover` trims a
     little from the sides; the subject sits centre frame, which is why the
     crop can breathe without losing him. */
  background:
    url('../assets/portrait.jpg?v=2') center / cover no-repeat,
    var(--portrait);
}

/* ── Projects ──────────────────────────────────────────── */

/* One project per row, image-led: a large thumbnail against the left
   gutter with the writing beside it. Column widths are percentages of the
   card's content box, so the proportion holds at any card width and the
   leftover space falls between the two. */
/* The box the rows travel inside: it starts below the nav and ends at the
   card's bottom gutter, which is what sets how far the list scrolls. It
   does not clip — rows run past both edges and are cut only by the card
   itself, so the list reads as one continuous reel passing through. */
.projects-view {
  flex: 1 1 auto;
  min-height: 0;
}

.projects {
  display: flex;
  flex-direction: column;
  gap: clamp(30px, 4.4vh, 44px);
  transform: translateY(calc(var(--list-y) * -1));
  will-change: transform;
}

.project {
  display: grid;
  /* An explicit gap rather than space-between: the old arrangement left
     whatever was over between the two columns, so the distance drifted
     with the card width. This holds the pair together at a set measure. */
  grid-template-columns: 55% minmax(0, 1fr);
  column-gap: clamp(24px, 2.8vw, 40px);
  /* The thumbnail is much taller than its text, so the writing is centred
     against it rather than pinned to its top edge. */
  align-items: center;
}
/* Kept in the stylesheet rather than the markup so the writing still comes
   first in the document, which is the order it should be read in. */
.project__thumb { order: -1; }

/* display:contents keeps text+thumb as direct grid children of .project
   while making the whole row one accessible link, for projects that have
   a case study to open. */
.project__link {
  display: contents;
  color: inherit;
  text-decoration: none;
}
.project__link:focus-visible .project__thumb { outline: 2px solid var(--ink); outline-offset: 3px; }

/* Set back from the thumbnail's scale: the text does not need to match the
   image's weight to hold the row, and the smaller size leaves the block
   shorter than the thumbnail, which is where the air in the row comes from. */
.project__text {
  display: flex;
  flex-direction: column;
  gap: 22px;
}
.project__head { display: flex; flex-direction: column; gap: 6px; }

.project__client {
  font-size: 15px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink-dim);
}
.project__title {
  font-size: clamp(18px, 1.55vw, 22px);
  font-weight: 600;
  letter-spacing: -.015em;
  line-height: 1.45;
}

.tags { display: flex; flex-wrap: wrap; gap: 8px; }
.tag {
  padding: 4px 8px;
  border-radius: 6px;
  background: var(--chip);
  color: var(--chip-ink);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.5;
  transition: background-color .5s var(--ease), color .5s var(--ease);
}

.project__thumb {
  width: 100%;
  aspect-ratio: 470 / 256;
  border-radius: 4px;
  background: var(--surface) center / cover no-repeat;
  overflow: clip;
  transition: background-color .5s var(--ease), opacity .3s var(--ease);
}
.project__thumb { position: relative; }
.project__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* A thumbnail whose blue is drawn rather than baked in. The gradient is the
   floor: it is what shows before the canvas mounts, when WebGL2 is missing,
   and whenever motion is unwelcome — so the tile is never empty. */
.project__thumb--live {
  position: relative;
  background: linear-gradient(105deg, #0651b5, #3ba1ff);
}
.project__thumb--live canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Crossfading slides, 1.2s held and .3s to cross. Negative delays offset each
   slide by one step, so slide n fades in exactly as n-1 fades out.

   The cycle is paused until the card is hovered or focused: a grid of tiles
   all animating at once is noise, and a still thumbnail is a better thing to
   scan. Paused at time zero the first slide is the one showing. */
.project__thumb .thumb__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  animation-timing-function: var(--ease);
  animation-iteration-count: infinite;
  animation-play-state: paused;
}
.project:hover .thumb__slide,
.project:focus-within .thumb__slide { animation-play-state: running; }

/* The resting image is its own layer above the cycle rather than the first
   frame of it. Hovering fades it out to reveal the loop; leaving fades it
   back over whatever frame the loop reached, so the thumbnail returns to the
   same picture every time instead of stopping wherever the cursor left it. */
.thumb__default {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 1;
  transition: opacity .4s var(--ease);
}
.project:hover .thumb__default,
.project:focus-within .thumb__default { opacity: 0; }

.project__thumb[data-slides="4"] .thumb__slide {
  animation-name: thumb-cycle-4;
  animation-duration: 6s;
}
.project__thumb[data-slides="4"] .thumb__slide:nth-of-type(2) { animation-delay: -4.5s; }
.project__thumb[data-slides="4"] .thumb__slide:nth-of-type(3) { animation-delay: -3s; }
.project__thumb[data-slides="4"] .thumb__slide:nth-of-type(4) { animation-delay: -1.5s; }

.project__thumb[data-slides="3"] .thumb__slide {
  animation-name: thumb-cycle-3;
  animation-duration: 4.5s;
}
.project__thumb[data-slides="3"] .thumb__slide:nth-of-type(2) { animation-delay: -3s; }
.project__thumb[data-slides="3"] .thumb__slide:nth-of-type(3) { animation-delay: -1.5s; }

@keyframes thumb-cycle-4 {
  0%,   20%   { opacity: 1; }
  25%,  95%   { opacity: 0; }
  100%        { opacity: 1; }
}
@keyframes thumb-cycle-3 {
  0%,   26.7% { opacity: 1; }
  33.3%, 93.3% { opacity: 0; }
  100%        { opacity: 1; }
}
}

/* ── Projects: layout options ──────────────────────────── */

/* Switchable with data-layout on #projectsList. No attribute, or "list",
   is the default above: text left, thumbnail right. */

/* The earlier arrangement: writing first, smaller thumbnail to its right. */
[data-layout="text-left"] .project {
  grid-template-columns: 40.7% 38.8%;
  justify-content: space-between;
  column-gap: 0;
  align-items: start;
}
[data-layout="text-left"] .project__thumb { order: 0; }
[data-layout="text-left"] .project__title { font-size: clamp(17px, 1.39vw, 20px); }

/* Two up. Thumbnail over text, so twice as many projects are in view at
   once and each one reads as a card rather than a line item. */
[data-layout="grid"].projects {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(28px, 3.4vh, 36px) clamp(20px, 2.2vw, 28px);
}
[data-layout="grid"] .project {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* thumbnail already leads by default */
[data-layout="grid"] .project__text { gap: 12px; }

/* An index. Type does the work and the thumbnail is held back until the
   row is pointed at, which suits a list that will keep growing. */
[data-layout="index"] .project {
  position: relative;
  grid-template-columns: 1fr;
  align-items: center;
  padding: clamp(14px, 1.9vh, 20px) 0;
  box-shadow: inset 0 -1px 0 var(--hairline);
}
[data-layout="index"] .project__text {
  flex-direction: row;
  align-items: baseline;
  gap: 16px;
}
[data-layout="index"] .project__head {
  flex-direction: row;
  align-items: baseline;
  gap: 16px;
}
[data-layout="index"] .project__client {
  flex: 0 0 auto;
  width: 92px;
}
[data-layout="index"] .project__title {
  font-size: clamp(20px, 2.1vw, 30px);
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.25;
  max-width: 62%;
}
[data-layout="index"] .tags { display: none; }
[data-layout="index"] .project__thumb {
  order: 0;
  position: absolute;
  top: 50%;
  right: 0;
  translate: 0 -50%;
  width: clamp(180px, 20vw, 260px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s var(--ease);
}
[data-layout="index"] .project:hover .project__thumb,
[data-layout="index"] .project:focus-within .project__thumb { opacity: 1; }

/* Floating cards, after the Nivis product layout: writing down the left,
   the shot filling the right.

   No markup of its own — the wrappers collapse with display:contents so the
   title, thumbnail, client and tags become direct grid children and can be
   placed individually. That keeps this a pure alternative to the other
   layouts rather than a second set of HTML. */
[data-layout="card"].projects { gap: clamp(20px, 2.6vh, 28px); }

[data-layout="card"] .project {
  display: grid;
  grid-template-columns: 34fr 62fr;
  /* Middle row takes the slack, so the chips are pushed to the foot of the
     card however long the title runs. */
  grid-template-rows: auto 1fr auto;
  column-gap: clamp(20px, 2.4vw, 34px);
  padding: clamp(13px, 1.4vw, 18px);
  border-radius: 14px;
  /* The card is the surface now: it takes the mode's own card colour and
     ink, and casts a shadow because it is sitting on the gradient rather
     than inside a panel. */
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 18px 44px rgba(0, 0, 0, .18);
  transition: background-color .5s var(--ease);
}

/* With each project carrying its own surface, the container behind them is
   dropped so the cards sit straight on the gradient. Scoped with :has() so
   only this layout clears it, and only on the fold that shows it. */
:root[data-fold="projects"] .card:has(.projects[data-layout="card"]) {
  background: transparent;
  /* The container clips its contents so the hero's card edge stays crisp.
     With that surface gone there is nothing to keep sharp, and the clip was
     slicing the cards at the top and bottom of the travel — so the cards
     run past it and off the screen instead. */
  overflow: visible;
}

/* The fold's own gutter is dropped so the cards span the container edge to
   edge — which puts them at exactly the hero card's width — and run to the
   bottom edge instead of stopping short of it. */
.fold--projects:has(.projects[data-layout="card"]) {
  padding-inline: 0;
  padding-bottom: 0;
}

/* The nav ink was chosen against a white container. With that gone it is
   dark text on a dark gradient, so it flips to the light side here. */
:root[data-fold="projects"] .card:has(.projects[data-layout="card"]) .nav,
:root[data-fold="projects"] .card:has(.projects[data-layout="card"]) .nav__home {
  color: #FFFFFF;
}

/* Collapse the wrappers so the leaves land in the grid directly. */
/* The link covers the card rather than only its contents, so the padding and
   the gaps between blocks are clickable too. The element itself is
   display: contents for layout, so the hit area is this overlay. */
[data-layout="card"] .project { position: relative; }
[data-layout="card"] .project__link::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 14px;
  z-index: 1;
}

[data-layout="card"] .project__link,
[data-layout="card"] .project__text,
[data-layout="card"] .project__head { display: contents; }

/* Left column, top to bottom: name, claim, chips. */
[data-layout="card"] .project__client {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
  font-size: clamp(16px, 1.4vw, 19px);
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.2;
  /* The same dim the client name takes in the other layouts, so it reads as
     a label above the claim rather than competing with it. */
  color: var(--ink-dim);
}

[data-layout="card"] .project__title {
  grid-column: 1;
  grid-row: 2;
  align-self: start;
  /* Carries the space the rule used to hold. */
  margin-top: 14px;
  font-size: clamp(19px, 2vw, 27px);
  font-weight: 600;
  letter-spacing: -.025em;
  line-height: 1.18;
  color: var(--ink);
  text-wrap: pretty;
}

[data-layout="card"] .tags {
  grid-column: 1;
  grid-row: 3;
  align-self: end;
  gap: 8px;
}

/* Right: the shot, holding the frames' own proportion at every width.

   It used to fill the card's height instead, with the card carrying a
   min-height — so as the viewport widened the shot kept getting wider
   against a capped height and the picture inside was cropped harder and
   harder. Pinning the ratio and letting the card take its height from the
   shot keeps the crop identical from a laptop to a full-width display. */
[data-layout="card"] .project__thumb {
  grid-column: 2;
  grid-row: 1 / 4;
  aspect-ratio: 1770 / 960;   /* the source frames' own ratio */
  height: auto;
  align-self: start;
  border-radius: 10px;
}

/* ── Scroll driver ─────────────────────────────────────── */

.scroll-driver { height: var(--scroll-height, 200vh); }

/* ── No JS ─────────────────────────────────────────────── */

.no-js .card { position: relative; inset: auto; margin: var(--pad-y) var(--pad-x); min-height: 756px; }
.no-js .fold { position: relative; inset: auto; opacity: 1; transform: none; }
.no-js .fold--hero { min-height: 640px; }
.no-js .projects { transform: none; }
.no-js .scroll-driver { display: none; }

/* ── Responsive ────────────────────────────────────────── */

/* Thumbnails are already fluid (100% of their grid column via aspect-ratio),
   so only the row itself needs a breakpoint: below this the text measure
   and the thumbnail can no longer sit side by side without either one
   getting too narrow to read. The image leads once they stack. */
@media (max-width: 900px) {
  .hero__foot { flex-direction: column; align-items: flex-start; gap: 28px; }
  .hero__aside { width: 100%; justify-content: space-between; }
  .nav { gap: 20px; }
  .nav__role { display: none; }

  .projects { gap: clamp(32px, 4vh, 44px); }
  .project { grid-template-columns: 1fr; gap: 20px; }
  .project__thumb { order: -1; }
  .project__text { gap: 16px; }
}

@media (max-width: 700px) {
  :root[data-mode] {
    --pad-x: 12px;
    --pad-y: 12px;
    --gutter: 20px;
    --title-size: clamp(34px, 9.6vw, 48px);
    --title-measure: none;   /* the card is narrower than the desktop measure */
  }

  .card { border-radius: 6px; }

  .nav { gap: 12px; font-size: 15px; }
  .nav__home { white-space: nowrap; }
  .switch__label { display: none; }   /* the orb alone at narrow widths */
  .clock { font-size: 12px; white-space: nowrap; }
  .clock__icon { width: 13px; height: 13px; }

  /* Cue drops to the bottom edge, bio and portrait sit above it. */
  .hero__foot { flex-direction: column-reverse; align-items: flex-start; gap: 26px; }
  .hero__cue { font-size: 12px; }

  /* The photograph stacks above the writing rather than beside it: at this
     width a column narrow enough to sit next to it is too narrow to read. */
  .hero__aside { width: 100%; flex-direction: column; align-items: flex-start; gap: 18px; }
  .bio { flex: 1 1 auto; min-width: 0; width: 100%; }
  .bio__copy { width: 100%; }
  /* order pulls it in front of the bio: in the markup the photograph follows
     the writing, which is the right reading order but the wrong stacking. */
  .portrait { order: -1; width: 96px; height: 108px; min-height: 0; align-self: flex-start; }

  /* The two links share a row again: the bio is full width now, so they fit
     without the wrap that split them onto two lines. */
  .bio__links { flex-wrap: nowrap; }

  .projects { gap: 32px; }

  /* The card's two columns do not survive this width: the text column comes
     out at 108px, which runs the claim to eleven lines and stacks the chips
     three deep. Everything goes to one column, shot first. Written at the
     same specificity as the layout it overrides, or the two-column rule
     above wins. */
  [data-layout="card"] .project {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto auto;
    row-gap: 14px;
  }
  [data-layout="card"] .project__thumb  { grid-column: 1; grid-row: 1; }
  [data-layout="card"] .project__client { grid-column: 1; grid-row: 2; }
  [data-layout="card"] .project__title  { grid-column: 1; grid-row: 3; margin-top: 0; font-size: 16px; line-height: 1.25; }
  [data-layout="card"] .tags {
    grid-column: 1;
    grid-row: 4;
    /* One row: the chips have the whole card width now, so there is no
       reason for them to break. */
    flex-wrap: nowrap;
    gap: 6px;
  }
  [data-layout="card"] .tag {
    padding: 3px 7px;
    font-size: 11px;
  }
}

/* ── Mode-switch transition ────────────────────────────── */

/* A soft-edged bloom, not a hard-edged wipe: the new mode reveals through a
   radial-gradient mask rather than a clipped circle, so the boundary is a
   feathered gradient band instead of a coin-edge cutout — closer in spirit
   to the grain-gradient shader's own soft transitions than to a sharp iris
   wipe. Typed via @property so the browser can interpolate the radius as a
   real value; browsers without it just resolve the mask at its start and
   end state, which still switches correctly, only without the animation. */
@property --reveal-radius {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 0%;
}

::view-transition-group(root) { animation-duration: 480ms; }

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;   /* the UA default cross-fades old↔new; the bloom replaces that */
}

@keyframes mode-reveal {
  from { --reveal-radius: 0%; }
  to   { --reveal-radius: 150%; }
}

.mode-switching::view-transition-old(root) { z-index: 1; }
.mode-switching::view-transition-new(root) {
  z-index: 2;
  --reveal-feather: clamp(120px, 22vmax, 420px);
  -webkit-mask-image: radial-gradient(
    circle at var(--reveal-x, 50%) var(--reveal-y, 50%),
    black 0,
    black calc(var(--reveal-radius) - var(--reveal-feather)),
    transparent calc(var(--reveal-radius) + var(--reveal-feather))
  );
  mask-image: radial-gradient(
    circle at var(--reveal-x, 50%) var(--reveal-y, 50%),
    black 0,
    black calc(var(--reveal-radius) - var(--reveal-feather)),
    transparent calc(var(--reveal-radius) + var(--reveal-feather))
  );
  animation: mode-reveal 480ms var(--ease) forwards;
}

/* ── Motion preferences ────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  :root { --hero-y: 0px !important; --proj-y: 0px !important; }

  /* No cycling — the resting image simply stays. */
  .thumb__slide { animation: none; opacity: 0; }
  .thumb__default { opacity: 1 !important; transition: none; }

  .switch--pulse .switch__orb { animation: none; }
  * { transition-duration: .01ms !important; animation-duration: .01ms !important; }

  /* No intro sequence: the card and its contents are simply there. */
  :root.intro .card,
  :root.intro .nav,
  :root.intro .hero__title,
  :root.intro .hero__foot { animation: none; }
}


/* ── Projects footer ───────────────────────────────────── */

/* The same credit row and wordmark the case studies close on, sitting at the
   foot of the project list. No background of its own: the gradient behind the
   cards runs straight through it. */
.fold--projects .foot {
  margin-top: clamp(64px, 7vw, 104px);
  background: none;
}
.fold--projects .foot__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-bottom: 18px;
}
.fold--projects .foot__note,
.fold--projects .foot__links a {
  font-family: var(--eyebrow-face, var(--ui));
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* Not --ink-dim: that is tuned for text on a card, and on the light ground
     the footer sits on it washes out. This is the page ink, held back. */
  color: rgba(26, 26, 26, .72);
}
.fold--projects .foot__links a:hover { color: var(--ink); }

/* The credit row sits on the page ground, not on a card, so in the blue and
   green modes it takes light type like the wordmark beside it — the card's
   own dark ink belongs to the cards. */
:root[data-mode="2"] .fold--projects .foot__note,
:root[data-mode="2"] .fold--projects .foot__links a,
:root[data-mode="3"] .fold--projects .foot__note,
:root[data-mode="3"] .fold--projects .foot__links a {
  color: rgba(255, 255, 255, .72);
}
:root[data-mode="2"] .fold--projects .foot__links a:hover,
:root[data-mode="3"] .fold--projects .foot__links a:hover { color: #fff; }
.fold--projects .foot__links {
  display: flex;
  gap: 28px;
  padding: 0;
  margin: 0;
  list-style: none;
}
.fold--projects .foot__links a {
  text-decoration: none;
  transition: color .3s var(--ease);
}
.fold--projects .foot__links a:hover { color: var(--ink); }
.fold--projects .foot__links a:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* Runs the full width of the card, past the inset the credit row keeps, the
   way the case studies carry it past the content column. The card clips, so
   the granules stop at its rounded corners.

   Dark granules, not light: this sits on the gradient rather than on white,
   and ink reads as a watermark where white reads as a glare. --dot is read by
   the canvas script, so the colour stays in the stylesheet. */
.fold--projects .foot__mark {
  --dot: rgba(0, 0, 0, .2);
}
/* The blue and green modes put the wordmark on a dark ground, where ink
   disappears and light granules read as the watermark instead. */
:root[data-mode="2"] .fold--projects .foot__mark,
:root[data-mode="3"] .fold--projects .foot__mark {
  --dot: rgba(255, 255, 255, .26);
}
.fold--projects .foot__mark {
  margin: 0;
  font-size: 0;
}
.fold--projects .foot__mark canvas { display: block; width: 100%; }

@media (max-width: 700px) {
  /* Credit and links share one row rather than stacking, and the wordmark is
     dropped: at this width it can only render as a band of noise. */
  .fold--projects .foot__bar { flex-direction: row; align-items: center; gap: 16px; }
  .fold--projects .foot__links { gap: 18px; }
  .fold--projects .foot__mark { display: none; }
}

/* ── Sidekick sheet ──────────────────────────────────────
   Lives in css/sheet.css now, because About opens the same panel and does
   not load this sheet. Both pages link it alongside their own styles. */
