/* =============================================================================
   Vittoria Fornari — portfolio
   A sketchbook you scroll through.
   ========================================================================== */

/* ------------------------------------------------------------------ fonts */
/* Milligram — four static cuts: Light 300, Regular 400, Medium 600, Italic.
   (Milligram has no true Semibold; Medium fills the 600 slot.) If you ever
   convert these to .woff2 they'll load a lot faster — add the woff2 line
   back to each src, ahead of the .otf.

   Milligram, and only Milligram. The four cuts below are the whole
   typographic system — nothing else is loaded or referenced anywhere.
   `font-display: block` rather than `swap` so a system face is never
   painted on the way in: the type shows up as Milligram or not at all.
   Only the .otf files exist, so those are the only formats listed —
   the old .woff2/.woff/.ttf lines pointed at files that were never
   there and just cost four 404s on every load. */
@font-face {
  font-family: "Display";
  src: url("../fonts/display-light.otf") format("opentype");
  font-weight: 300;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "Display";
  src: url("../fonts/display.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "Display";
  src: url("../fonts/display-semibold.otf") format("opentype");
  font-weight: 600;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "Display";
  src: url("../fonts/display-italic.otf") format("opentype");
  font-style: italic;
  font-weight: 400;
  font-display: block;
}

/* ----------------------------------------------------------------- tokens */
:root {
  /* White ground, dark type. */
  --paper:        #FFFFFF;
  --paper-deep:   #E7E5DF;
  --sheet:        #F7F5F0;
  --sheet-warm:   #F2EFE8;

  --ink:          #14151A;
  --graphite:     #5C5E63;
  --pencil:       #9A9CA1;

  --accent:       #14151A;

  --tape:         rgba(230, 223, 191, .72);
  --tape-edge:    rgba(196, 187, 150, .55);

  --font-display: "Display", sans-serif;   /* Milligram */
  --font-body:    "Display", sans-serif;   /* Milligram */
  --font-hand:    "Display", sans-serif;   /* Milligram, set in italic below */

  /* mobile keeps a normal small margin; the desktop breakpoint below pins
     this to an exact 50px on request, rather than a fluid clamp. */
  --gutter: 1rem;
  --measure: 62ch;

  --swipe-yellow: #FFDD00;
  --swipe-pink:   #ED86DA;
  --swipe-orange: #FF8000;
  --swipe-blue:   #00E8FF;

  /* one of the hover-scribble's own squiggle paths (path 0), reused as a
     stretchable mask so every plain divider line on the site is drawn in
     the same hand-wavy stroke instead of a ruler-straight rule. Non-uniform
     stretch per element is intentional — the animated hover version does
     the same horizontal-only rescale in main.js. */
  --scribble-rule: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 310 40' preserveAspectRatio='none'%3E%3Cpath d='M5 20.9999C26.7762 16.2245 49.5532 11.5572 71.7979 14.6666C84.9553 16.5057 97.0392 21.8432 109.987 24.3888C116.413 25.6523 123.012 25.5143 129.042 22.6388C135.981 19.3303 142.586 15.1422 150.092 13.3333C156.799 11.7168 161.702 14.6225 167.887 16.8333C181.562 21.7212 194.975 22.6234 209.252 21.3888C224.678 20.0548 239.912 17.991 255.42 18.3055C272.027 18.6422 288.409 18.867 305 17.9999' stroke='%23000' stroke-width='3' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
}

@media (min-width: 761px) {
  :root { --gutter: 50px; }   /* desktop side margin, exactly as asked */
}

/* ------------------------------------------------------------------ reset */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: var(--font-body);
  font-size: clamp(0.975rem, 0.92rem + 0.22vw, 1.0625rem);
  line-height: 1.62;
  color: var(--ink);
  background-color: var(--paper);
  overflow-x: hidden;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
h1, h2, h3, h4 { margin: 0; font-weight: 600; line-height: 1.06; }
p { margin: 0 0 1em; }
button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer; }

/* (the paper-grain overlay was removed - the background is flat cream) */

/* --lime/--violet were never defined in :root, so these silently fell back
   to the browser default — fixed to use tokens that actually exist. */
::selection { background: var(--swipe-yellow); color: var(--ink); }
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.u-hidden { display: none !important; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* =================================================================== ABOUT */
/* No frame — no mat, shadow, padding or tilt. Just the photograph, with a
   plain caption underneath rather than overlaid on a card. */
.portrait { max-width: 340px; }
.portrait__shot { position: relative; aspect-ratio: 1 / 1; overflow: hidden; background: var(--paper-deep); }
/* the .slot placeholder underneath is position:absolute, so without its
   own stacking position the (plain, static) img was actually painting
   BELOW it — the photo was there, just permanently hidden behind the
   grey placeholder. Same bug already fixed on .plate__shot img. */
.portrait__shot img { width: 100%; height: 100%; object-fit: cover; position: relative; z-index: 1; display: block; }
.portrait figcaption {
  margin-top: .7rem;
  font-size: .8125rem;
  font-style: italic;
  color: var(--graphite);
}
.about__body p { max-width: none; }
.rule {
  margin: 2.25rem 0 1.1rem;
  padding-bottom: .7rem;
  font-size: .8125rem;
  color: var(--graphite);
  background-image: var(--scribble-rule);
  background-repeat: no-repeat;
  background-size: 100% 10px;
  background-position: left bottom;
}
.stack { list-style: none; margin: 0; padding: 0; }
.stack li {
  padding: .6rem 0 .9rem;
  background-image: var(--scribble-rule);
  background-repeat: no-repeat;
  background-size: 100% 8px;
  background-position: left bottom;
}
.stack li:last-child { background-image: none; }
.stack strong { font-weight: 600; }
.stack span { display: block; font-size: .8125rem; color: var(--graphite); }

/* ================================================================= CONTACT */
.colophon {
  position: relative;
  z-index: 3;
  padding: clamp(3.5rem, 9vh, 6.5rem) var(--gutter) 2.5rem;
  margin: 0 auto;
  background-image: var(--scribble-rule);
  background-repeat: no-repeat;
  background-size: 100% 12px;
  background-position: left top;
}
.colophon h2 {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(2.2rem, 8vw, 5.5rem);
  text-transform: uppercase;
  letter-spacing: -.09em;
  line-height: .96;
}
.colophon__sub {
  max-width: none;
  margin: 1rem 0 2.2rem;
  color: var(--graphite);
  font-size: clamp(.7rem, 1.6vw, 1.15rem);
  line-height: 1.55;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* the email CTA — a button in the same small-caps family as everything
   else now, not a filled pill; the little dot still marks it as live */
.colophon__email {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  margin-bottom: 2.4rem;
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  --swipe-colour: var(--swipe-blue);
}
.colophon__email-i {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.colophon__email[hidden] { display: none; }
.colophon__links { display: flex; flex-wrap: wrap; gap: 1.4rem 2.2rem; }
.colophon__links a {
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-decoration: none;
}
.colophon__links a:nth-child(4n+1) { --swipe-colour: var(--swipe-yellow); }
.colophon__links a:nth-child(4n+2) { --swipe-colour: var(--swipe-pink); }
.colophon__links a:nth-child(4n+3) { --swipe-colour: var(--swipe-orange); }
.colophon__links a:nth-child(4n+4) { --swipe-colour: var(--swipe-blue); }
.colophon__foot {
  margin-top: clamp(2.5rem, 6vh, 4rem);
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--pencil);
  display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
}
.colophon__foot a { text-decoration: none; --swipe-colour: var(--swipe-yellow); }

/* ============================================================== CASE STUDY */
.study {
  position: fixed;
  inset: 0;
  z-index: 700;
  overflow-y: auto;
  overscroll-behavior: contain;
  background-color: var(--paper);   /* flat cream */
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .34s ease, transform .38s cubic-bezier(.2,.8,.3,1);
}
.study[hidden] { display: none; }
.study.is-open { opacity: 1; transform: none; }

/* ============================================================== RESPONSIVE */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}

/* print — because a portfolio should survive being printed */
@media print {
  .masthead { display: none; }
  body { background: #fff; }
}

/* ===========================================================================
   THE MAP
   ---------------------------------------------------------------------------
   The homepage. Every project is a node; a line joins two projects that share
   a discipline, drawn heavier the more of their disciplines overlap. Nodes
   ease out of the cursor's way and grow when you point at one. Positions come
   from a small force layout in main.js, so nothing here is hand-placed.
   ========================================================================= */
.map {
  position: relative;
  /* the masthead is fixed, so the map has to clear it, but no more than that */
  padding: clamp(5.5rem, 9vh, 7rem) var(--gutter) clamp(1.5rem, 4vh, 3rem);
  max-width: 1400px;
  margin: 0 auto;
}
/* the two lines pinned to the true screen edges, not the (centred,
   max-width) map container's own edges — the 50vw trick breaks them out to
   the real viewport margin regardless of how wide the container itself is.
   Horizontal text in its own boxed column; .map__field below is narrowed at
   the same breakpoint so that column is genuinely reserved space, not just
   text floating over wherever the map nodes happen to land. */
.edge {
  position: absolute;
  top: 54%;
  translate: 0 -50%;
  width: 11rem;
  margin: 0;
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--pencil);
  line-height: 1.55;
  pointer-events: none;
}
.edge--left  { left:  calc(50% - 50vw + 1.25rem); text-align: left; }
.edge--right { right: calc(50% - 50vw + 1.25rem); text-align: right; }

@media (max-width: 1160px) {
  .edge { display: none; }   /* no room beside the map at this width */
}

.map__field {
  position: relative;
  height: clamp(460px, 78vh, 820px);
  touch-action: pan-y;
}
@media (min-width: 1161px) {
  /* reserves a real column for .edge on each side, inside .map's own content
     box — not just relying on empty space that may or may not exist once
     .map's max-width stops centring it away from the viewport edge */
  .map__field { max-width: calc(100% - 2 * 14rem); margin: 0 auto; }
}
.map__links {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
/* solid = these two share a discipline, weighted by how much.
   dashed = they share nothing, drawn only so the map stays one piece. */
.link {
  fill: none;
  stroke: var(--graphite);
  stroke-linecap: round;
  opacity: var(--o, .3);
  transition: opacity .32s ease, stroke .32s ease;
}
.link--far {
  stroke-dasharray: 2 7;
  stroke: var(--pencil);
}
.link.is-on { stroke: var(--ink); opacity: .9; }

.map__nodes { position: absolute; inset: 0; }

/* every node is positioned by its transform, so the anchor itself is a
   zero-size point and the picture is centred on it */
.node {
  position: absolute;
  top: 0; left: 0;
  width: 0; height: 0;
  display: block;
  text-decoration: none;
  color: inherit;
  will-change: transform;
}
.node__shot {
  position: absolute;
  left: 50%; top: 50%;
  /* a touch smaller than before: the archive circles added three more
     bodies to the same field, and at the old size the six project pictures
     could no longer be separated inside it. */
  width: clamp(104px, 11vw, 158px);
  aspect-ratio: 1 / 1;
  translate: -50% -50%;
  overflow: hidden;
  background: var(--paper-deep);
  box-shadow: 0 1px 2px rgba(40,38,30,.14), 0 10px 22px -14px rgba(40,38,30,.5);
  transition: scale .34s cubic-bezier(.2,.8,.3,1), box-shadow .34s ease, opacity .32s ease;
}
.node__shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

.node.is-on  .node__shot { scale: 1.44; box-shadow: 0 4px 10px rgba(40,38,30,.2), 0 26px 50px -22px rgba(40,38,30,.55); z-index: 5; }
.node.is-on  { z-index: 5; }
.node.is-off .node__shot { opacity: .3; }

/* the coordinate slug that sits above each picture */
/* the coordinate slug sits above every picture, always readable — the name
   only joins it when you point at one */
.node__tag {
  position: absolute;
  left: 50%; top: 50%;
  translate: -50% calc(-50% - clamp(74px, 7.4vw, 108px));
  display: block;
  width: max-content;
  max-width: 15rem;
  text-align: center;
  font-size: .625rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--pencil);
  opacity: 1;
  transition: opacity .28s ease, translate .34s cubic-bezier(.2,.8,.3,1), color .28s ease;
  pointer-events: none;
}
.node__co   { display: block; font-variant-numeric: tabular-nums; }
.node__name {
  display: block;
  color: var(--ink);
  font-weight: 600;
  opacity: 0;
  transition: opacity .28s ease;
}
/* the node picture scales 1.44x on hover (see .node__shot above), so the
   tag needs to clear HALF of that enlarged size, not the resting one — the
   old fixed clamp barely cleared the biggest picture on a wide screen. calc()
   inside clamp lets the offset track the shot's own responsive width
   directly instead of guessing a matching number by hand. */
.node.is-on .node__tag  { translate: -50% calc(-50% - clamp(112px, calc(9vw + 26px), 168px)); color: var(--graphite); }
.node.is-on .node__name { opacity: 1; }
.node.is-off .node__tag { opacity: .4; }

/* the archive: side projects ride along as small circles, so they read as
   companions to the constellation rather than nodes in it. Same hover
   behaviour, just quieter and rounder. */
.node--arch .node__shot {
  width: clamp(58px, 6vw, 92px);
  border-radius: 50%;
  box-shadow: 0 1px 2px rgba(40,38,30,.12), 0 6px 14px -10px rgba(40,38,30,.45);
}
.node--arch .node__tag { translate: -50% calc(-50% - clamp(44px, 4.2vw, 64px)); }
.node--arch.is-on .node__shot { scale: 1.3; }
.node--arch.is-on .node__tag { translate: -50% calc(-50% - clamp(60px, calc(5vw + 16px), 92px)); }

/* the filter: the chosen kind of work comes forward, the rest steps back */
.node.is-lit  { z-index: 4; }
.node.is-lit  .node__shot { box-shadow: 0 3px 8px rgba(40,38,30,.2), 0 20px 40px -20px rgba(40,38,30,.55); }
.node.is-mute .node__shot { opacity: .16; }
.node.is-mute .node__tag  { opacity: .16; }

@media (max-width: 860px) {
  .map { padding-top: clamp(3.5rem, 8vh, 4.5rem); }
  .filter { padding-bottom: clamp(.4rem, 1.2vh, .7rem); }
  .map__field { height: clamp(380px, 62vh, 560px); }
  .node__shot { width: clamp(84px, 23vw, 124px); }
  .node--arch .node__shot { width: clamp(46px, 12vw, 68px); }
}

@media (max-width: 760px) {
  /* narrower side margin than the 50px desktop edge — the map wants the
     room back on a phone so the constellation has space to spread. */
  .map { padding-left: 20px; padding-right: 20px; padding-top: clamp(3rem, 8vh, 4.5rem); }
  /* bigger than before, with the collision buffer in main.js tightened to
     match (see gapFactor) — a very thin gap between photos instead of real
     air, without letting them touch. Sized up another 20% on request. */
  .node__shot { width: clamp(60px, 14.4vw, 91px); }
  .node--arch .node__shot { width: clamp(30px, 7vw, 42px); }
  /* taller field, so the extra width goes toward real spacing rather than
     tighter packing */
  .map__field { height: clamp(560px, 80vh, 740px); }
}




/* ===========================================================================
   DARK EDITORIAL
   ---------------------------------------------------------------------------
   Quiet dark ground, light type, a lot of air, and small uppercase markers in
   double brackets doing the signposting. Large text runs Light; small text
   runs uppercase and tracked out. All Milligram.
   ========================================================================= */
.mark {
  display: inline-block;
  font-size: .625rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--pencil);
}

.lede {
  max-width: 62rem;
  margin: 0 auto clamp(2.5rem, 7vh, 5rem);
  padding: 0 var(--gutter);
  text-align: center;
}
.lede h2 {
  margin: .9rem 0 0;
  line-height: 0;   /* the h2 now wraps a drawn logo image, not set type */
}
/* the "Works" / "About" section titles: hand-drawn logos, same style as the
   navbar wordmark. Sized by height so both read at the same visual weight
   even though the two source files have different width-to-height ratios. */
.lede__art {
  height: clamp(2.6rem, 7vw, 4.6rem);
  width: auto;
  display: inline-block;
}
.lede p {
  margin: 1.1rem auto 0;
  max-width: 34rem;
  color: var(--graphite);
  font-size: clamp(.95rem, 1.4vw, 1.0625rem);
  line-height: 1.6;
}

/* ------------------------------------------------------------- masthead */
/* Wordmark hard left, the sections in a flat row, the clock and the city
   hard right. Everything but the wordmark is small, uppercase and tracked
   out, set on a single baseline grid. */
.masthead {
  position: fixed;
  inset: 0 0 auto;
  z-index: 800;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
  gap: 1.5rem;
  padding: 1.15rem var(--gutter);
  background: linear-gradient(var(--paper) 64%, transparent);
  transition: transform .38s cubic-bezier(.2,.8,.3,1), background .3s ease, box-shadow .3s ease;
}
/* main.js tracks scroll direction: hides going down past the hero, and
   reappears going up — with a translucent white ground instead of the
   top-of-page gradient, since by then it's floating over real content
   rather than blending into the map. */
.masthead.is-hidden { transform: translateY(-115%); }
.masthead.is-floating {
  background: rgba(255,255,255,.82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 1px 0 rgba(20,21,26,.06);
}
.wordmark {
  justify-self: start;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.9vw, 1.6rem);
  font-weight: 600;
  letter-spacing: -.02em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  line-height: 1;
}
.masthead__nav {
  justify-self: center;
  display: flex;
  gap: clamp(1.5rem, 5vw, 4rem);
}
.masthead__nav a {
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  transition: color .2s ease;
}
.masthead__nav a:hover { color: var(--pencil); }
.count { font-weight: 400; }

.place {
  justify-self: end;
  display: flex;
  flex-direction: column;
  gap: .15rem;
  text-align: right;
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------ mobile: a burger */
.burger {
  display: none;
  width: 34px; height: 34px;
  justify-self: end;
  padding: 0;
}
.burger__svg { width: 26px; height: 18px; overflow: visible; display: block; margin: 0 auto; }
.burger__bar {
  fill: none;
  stroke: var(--ink);
  stroke-width: 2.4;
  stroke-linecap: round;
  transform-box: fill-box;
  transform-origin: center;
  transition: transform .4s cubic-bezier(.4,0,.2,1);
}
/* the bars turn into an X on open — a plain CSS transform, with the
   redrawn-stroke flourish (main.js) layered on top of every toggle */
.burger.is-open .burger__bar--a { transform: translateY(7px) rotate(45deg); }
.burger.is-open .burger__bar--b { transform: translateY(-7px) rotate(-45deg); }

@media (max-width: 760px) {
  /* a short bar: logo and burger share one tight row, nav and place fold
     into a drawer underneath — clock-and-city first, then the sections. */
  .masthead {
    grid-template-columns: 1fr auto;
    grid-template-areas: "logo burger" "place place" "nav nav";
    align-items: center;
    gap: 0 1rem;
    padding: .5rem var(--gutter);
  }
  .wordmark { grid-area: logo; }
  .burger { grid-area: burger; align-self: center; }
  .place { grid-area: place; }
  .masthead__nav { grid-area: nav; }

  .burger { display: grid; place-items: center; }

  /* nav and place fold into a drawer under the bar, closed by default.
     Both blocks sit flush on the right, under the burger — not the logo. */
  .masthead__nav, .place {
    justify-self: end;
    align-items: flex-end;
    flex-direction: column;
    gap: .9rem;
    max-height: 0;
    opacity: 0;
    overflow: hidden;
    pointer-events: none;
    transition: max-height .4s cubic-bezier(.4,0,.2,1), opacity .3s ease;
  }
  .place { text-align: right; }
  /* a soft white scrim behind the open drawer, so the section links and
     clock read clearly over whatever's on the map behind them */
  body.nav-open .masthead {
    background: rgba(255,255,255,.86);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }
  body.nav-open .masthead__nav,
  body.nav-open .place {
    max-height: 16rem;
    opacity: 1;
    pointer-events: auto;
  }
  /* real breathing room around the clock/location block — above it (clear
     of the logo row) and below it (clear of the section links) — gated to
     the open state so the collapsed bar stays tight. */
  body.nav-open .place { padding-top: 1.6rem; padding-bottom: .4rem; }
  body.nav-open .masthead__nav { padding-top: 1.6rem; }
}

/* the scroll cue, centred at the foot of the first screen */
.cue {
  display: block;
  width: max-content;
  margin: clamp(2rem, 5vh, 3.5rem) auto 0;
  font-size: .6875rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  transition: color .22s ease;
}
.cue:hover { color: var(--pencil); }

/* ------------------------------------------------------------ the works */
.works { padding: clamp(4rem, 12vh, 9rem) 0 clamp(3rem, 9vh, 6rem); }
.works__grid {
  list-style: none;
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  /* three loose columns of small pictures rather than two big ones, each
     item narrower than its own column and dropped to a different height,
     so the page reads as scattered prints rather than a tidy grid. */
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(3rem, 7vw, 6rem) clamp(1.5rem, 3vw, 3rem);
  align-items: start;
}
/* the stagger: fixed, hand-set widths and offsets per position rather than
   anything random, so the arrangement is the same on every load. Each item
   is narrower than its own column, which is what leaves the air between
   them and stops the row reading as a tidy grid. */
.work { width: min(100%, 21rem); }
.work:nth-child(6n+1) { width: min(100%, 20rem); justify-self: end;    margin-top: 0; }
.work:nth-child(6n+2) { width: min(100%, 22rem); justify-self: center; margin-top: clamp(3rem, 9vh, 7rem); }
.work:nth-child(6n+3) { width: min(100%, 21rem); justify-self: start;  margin-top: clamp(.5rem, 1.5vh, 1.2rem); }
.work:nth-child(6n+4) { width: min(100%, 22rem); justify-self: end;    margin-top: clamp(2rem, 6vh, 4.5rem); }
.work:nth-child(6n+5) { width: min(100%, 19rem); justify-self: center; margin-top: clamp(5rem, 14vh, 11rem); }
.work:nth-child(6n+6) { width: min(100%, 21rem); justify-self: start;  margin-top: 0; }
.work__a {
  display: block;
  text-decoration: none;
  color: inherit;
  position: relative;
}
.work:nth-child(4n+1) .work__a { --swipe-colour: var(--swipe-yellow); }
.work:nth-child(4n+2) .work__a { --swipe-colour: var(--swipe-pink); }
.work:nth-child(4n+3) .work__a { --swipe-colour: var(--swipe-orange); }
.work:nth-child(4n+4) .work__a { --swipe-colour: var(--swipe-blue); }
.work__no {
  position: absolute;
  top: .7rem; left: .7rem;
  z-index: 2;
  font-size: .625rem;
  letter-spacing: .14em;
  color: var(--ink);
  mix-blend-mode: difference;
}
/* no forced crop here any more — each cover keeps its own proportions, which
   is what gives the row its uneven, pinned-up look. */
.work__shot {
  display: block;
  position: relative;
  overflow: hidden;
  background: var(--paper-deep);
}
.work__shot img {
  width: 100%; height: auto;
  display: block;
  transition: scale .7s cubic-bezier(.2,.8,.3,1);
  /* full colour at rest and on hover now — the grayscale(.35) that used to
     lift on hover is gone, on request */
}
.work__a:hover .work__shot img,
.work__a:focus-visible .work__shot img { scale: 1.05; }

/* title on its own line, then the disciplines with the year pushed out to
   the far edge of the caption */
.work__meta {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .15rem .8rem;
  margin-top: .9rem;
}
.work__title {
  grid-column: 1 / -1;
  font-family: var(--font-display);
  font-size: .9375rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: -.06em;
}
.work__year { font-size: .625rem; letter-spacing: .12em; color: var(--pencil); text-align: right; align-self: end; }
.work__cat {
  font-size: .625rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--graphite);
}
.work__go {
  display: block;
  margin-top: .7rem;
  font-size: .625rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--pencil);
  opacity: 0;
  translate: 0 4px;
  transition: opacity .3s ease, translate .3s ease, color .3s ease;
}
.work__a:hover .work__go,
.work__a:focus-visible .work__go { opacity: 1; translate: 0; color: var(--ink); }

/* ------------------------------------------------------------ the about */
.about { padding: clamp(4rem, 12vh, 9rem) 0 clamp(3rem, 9vh, 6rem); }

/* the big opening line: full width, left-aligned, sits above the grid below
   rather than being the grid's own first paragraph */
.about__headline {
  margin: 0 auto clamp(3rem, 9vh, 5.5rem);
  padding: 0 var(--gutter);
  text-align: left;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.6rem, 3.6vw, 2.75rem);
  line-height: 1.25;
  letter-spacing: -.01em;
  color: var(--graphite);
}
.about__headline strong { color: var(--ink); font-weight: 600; }

.about__grid {
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: minmax(0, .85fr) minmax(0, 1fr);
  gap: clamp(3rem, 8vw, 7rem);
  align-items: start;
}
/* the photo sits noticeably lower than the text beside it, the way a
   sticky-note polaroid would actually be pinned unevenly with the copy */
.portrait { margin-top: clamp(2.5rem, 7vh, 4.5rem); }
/* capped independently of the grid track, so the column doesn't stretch
   text to fill all the room the grid gives it — the same right-hand
   whitespace the reference layout has */
.about__body { max-width: none; }
.about__body p { color: var(--graphite); font-size: clamp(1rem, 1.6vw, 1.1875rem); line-height: 1.7; }

.disciplines {
  margin: clamp(3rem, 8vh, 5rem) auto 0;
  padding: 0 var(--gutter);
}
.disciplines ul {
  position: relative;   /* the SVG strings overlay this, positioned by main.js */
  list-style: none;
  margin: 1.2rem 0 0;
  padding: 0;
}
.disciplines li {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  align-items: center;
  gap: .4rem 2rem;
  padding: 1.3rem 0;
  /* no border here any more — the line between/around rows is drawn by the
     pluckable SVG strings in main.js, not CSS, so it can bend */
}
/* the ability name: big, bold, uppercase — one flat colour now (black),
   was five different accent colours per row before this was asked to be
   plain, and before that a neon-style glow behind each word. */
.disciplines strong {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: -.01em;
  font-size: clamp(1.5rem, 3.4vw, 2.4rem);
  line-height: 1;
  color: var(--ink);
}
.disciplines span { display: block; color: var(--graphite); font-size: .9375rem; text-align: right; }

/* --------------------------------------------------------- the harp strings */
.disciplines__strings {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  pointer-events: none;   /* only the individual hit-paths below opt back in */
  overflow: visible;
}
.string__line { fill: none; stroke: var(--paper-deep); stroke-width: 1; }
.string__hit {
  fill: none;
  stroke: transparent;
  stroke-width: 26;         /* a generous, invisible grab area around the thin line */
  pointer-events: stroke;
  cursor: grab;
  touch-action: none;       /* dragging a string shouldn't also scroll the page */
}
@media (prefers-reduced-motion: reduce) {
  .string__hit { cursor: default; pointer-events: none; }
}

@media (max-width: 760px) {
  .about__grid { grid-template-columns: 1fr; }
  .portrait { margin-top: 0; }
  .about__body { max-width: none; }
  .disciplines li { grid-template-columns: 1fr; }
  /* one column on a phone, and the desktop stagger/offsets come back off —
     there's no room for scattering at this width. Centred rather than
     stretched full-bleed, so each picture reads as its own card. */
  .works__grid { grid-template-columns: 1fr; justify-items: center; gap: clamp(2.5rem, 8vh, 4rem); }
  .work { width: min(100%, 24rem); }
  .works__grid .work { justify-self: center; margin-top: 0; }
}

@media (min-width: 761px) and (max-width: 1100px) {
  /* two columns in the middle band, with the stagger re-keyed to pairs */
  .works__grid { grid-template-columns: repeat(2, 1fr); }
  .work:nth-child(4n+1) { justify-self: end;   margin-top: 0; }
  .work:nth-child(4n+2) { justify-self: start; margin-top: clamp(3rem, 9vh, 6rem); }
  .work:nth-child(4n+3) { justify-self: end;   margin-top: clamp(1rem, 3vh, 2rem); }
  .work:nth-child(4n+4) { justify-self: start; margin-top: clamp(4rem, 12vh, 8rem); }
}

/* --------------------------------------------------------- case studies */
/* "back to the map" sits on its own line above the title; the title itself
   is set lowercase, medium-weight, tightly tracked. */
.study__inner { margin: 0 auto; padding: 0 var(--gutter) clamp(4rem, 12vh, 8rem); }
.study__head {
  padding: clamp(5.5rem, 12vh, 8.5rem) 0 0;
}
.study__head__row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .9rem;
}
.study__back {
  font-size: .6875rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--graphite);
  --swipe-colour: var(--swipe-blue);
}
.study__back:hover { color: var(--ink); }
.study__head h1 {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  font-size: clamp(2.4rem, 7.5vw, 5.5rem);
  letter-spacing: -.09em;
  line-height: .95;
  margin: 0;
}
.study__lede {
  margin: 1.6rem 0 0;
  max-width: none;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 40px;
  line-height: 1.25;
  color: var(--ink);
  opacity: .8;
}
.study__link {
  display: inline-block;
  margin-top: 1.4rem;
  font-size: .625rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink);
  padding-bottom: .3rem;
  text-decoration: none;
  --swipe-colour: var(--swipe-blue);
}

/* the year / service / role strip under the title, sourced from the
   portfolio deck — three columns of grey label + bold stacked values. */
.study__meta {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  align-items: start;
  gap: 1rem 2rem;
  margin: 20px 0;
  padding-top: 1.4rem;
  border-top: 1px solid var(--paper-deep);
}
.study__meta__col { display: flex; flex-direction: column; gap: .4rem; }
.study__meta__label { font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase; color: var(--pencil); }
.study__meta__col strong { font-weight: 600; font-size: .875rem; letter-spacing: -.01em; color: var(--ink); }

/* a text-bearing section gets the same gap above its heading as it does
   below its text, before the picture row — symmetric top/bottom air. A
   textless section (a solo picture in the rhythm) gets a much smaller gap
   instead, so consecutive solo pictures sit close together. */
.spread { padding: clamp(2.5rem, 7vh, 4.5rem) 0 0; }
.spread:not(:has(.spread__text)) { padding-top: clamp(.2rem, .5vh, .3rem); }
.spread__text { max-width: none; }
.spread__text h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  letter-spacing: -.015em;
  margin: 0 0 .9rem;
}
.spread__text p { color: var(--graphite); font-size: 1.0625rem; line-height: 1.7; margin: 0; }
/* the reference brief: large, generously-scaled images. A lone image (the
   1s in the 1-3-2-2-1 rhythm) fills the row; a group of 2-3 still gets real
   size each, not a cramped minimum. */
.spread__shots {
  margin-top: clamp(2.5rem, 7vh, 4.5rem);
  display: grid;
  /* min(420px, 100%) instead of a bare 420px — a narrow phone can't offer
     420px of column, and a bare minimum there just overflows the viewport
     instead of shrinking. */
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  gap: clamp(1.2rem, 2.6vw, 2rem);
}
.plate { margin: 0; }
/* every plate — cover included — is cropped to a fixed frame instead of
   showing at its native size, so a row of images lines up evenly no matter
   what shape the source photo came in. */
.plate__shot { display: block; position: relative; overflow: hidden; background: var(--paper-deep); aspect-ratio: 4 / 3; }
.plate__shot img { width: 100%; height: 100%; object-fit: cover; display: block; position: relative; z-index: 1; }

/* a lone image — the cover, or any single-picture group in the rhythm —
   runs full width, so aspect-ratio:4/3 alone would make it enormous on a
   wide screen. Cap the height instead so it sits comfortably within a
   1920×1080 view; object-fit:cover still crops it in cleanly. 200px taller
   than the original cap, on request. */
.study__inner > .plate .plate__shot,
.spread__shots > .plate:only-child .plate__shot {
  aspect-ratio: auto;
  height: clamp(520px, calc(58vh + 200px), 960px);
}

/* a three-picture group reads as one big spread up top with two smaller
   plates underneath it, rather than three equal columns — desktop only;
   mobile keeps the plain single-column stack. */
@media (min-width: 761px) {
  .spread__shots:has(> .plate:nth-child(3):last-child) {
    grid-template-columns: 1fr 1fr;
  }
  .spread__shots:has(> .plate:nth-child(3):last-child) > .plate:first-child {
    grid-column: 1 / -1;
  }
  .spread__shots:has(> .plate:nth-child(3):last-child) > .plate:first-child .plate__shot {
    aspect-ratio: 16 / 9;
  }
}

@media (max-width: 760px) {
  /* the "about the project" intro reads oversized at 40px on a phone */
  .study__lede { font-size: 26px; }
  /* symmetric top/bottom air around a text section on mobile too, but the
     solo pictures still get a much tighter gap between each other */
  .spread { padding-top: clamp(1.8rem, 6vh, 3rem); }
  .spread:not(:has(.spread__text)) { padding-top: clamp(.15rem, .5vh, .25rem); }
  /* the year/service/role strip stacks to one column on a phone; the
     arrow doesn't earn its space at this width */
  .study__meta { grid-template-columns: 1fr; gap: 1.1rem; }
  /* on a phone there's no row to line up, so let every case-study picture
     show at its own aspect ratio instead of being cropped to a fixed frame */
  .plate__shot,
  .study__inner > .plate .plate__shot,
  .spread__shots > .plate:only-child .plate__shot {
    aspect-ratio: auto;
    height: auto;
  }
  .plate__shot img { height: auto; object-fit: contain; }
}

/* ------------------------------------------------------------- lightbox */
/* The archive opens here, on top of the landing — not a page of its own.
   A dim backdrop, a close button, a title, then the pictures in one
   horizontal-scrolling strip at their own proportions. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: grid;
  place-items: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity .28s ease;
}
.lightbox.is-open { opacity: 1; pointer-events: auto; }
.lightbox__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(20, 21, 26, .78);
  backdrop-filter: blur(2px);
}
.lightbox__panel {
  position: relative;
  z-index: 1;
  width: min(92vw, 1400px);
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  translate: 0 12px;
  transition: translate .32s cubic-bezier(.2,.8,.3,1);
}
.lightbox.is-open .lightbox__panel { translate: 0 0; }
.lightbox__close {
  position: absolute;
  top: -2.6rem;
  right: 0;
  width: 2.2rem;
  height: 2.2rem;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--paper);
  display: grid;
  place-items: center;
}
.lightbox__title {
  color: var(--paper);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.2rem, 2.4vw, 1.7rem);
  margin: 0 0 1rem;
}
.lightbox__strip {
  display: flex;
  gap: 1.2rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-bottom: .5rem;
}
.lightbox__plate { margin: 0; flex: none; height: min(72vh, 640px); scroll-snap-align: center; }
.lightbox__shot { display: inline-block; position: relative; height: 100%; background: var(--paper-deep); }
.lightbox__shot img { height: 100%; width: auto; max-width: none; display: block; position: relative; z-index: 1; object-fit: contain; }

@media (max-width: 760px) {
  .lightbox__panel { width: 100vw; padding: 0 1rem; }
  .lightbox__close { top: .6rem; right: .6rem; color: var(--ink); }
  .lightbox__plate { height: min(60vh, 480px); }
}

.study__foot {
  margin-top: clamp(3rem, 10vh, 6rem);
  padding-top: 1.6rem;
  background-image: var(--scribble-rule);
  background-repeat: no-repeat;
  background-size: 100% 10px;
  background-position: left top;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  justify-content: space-between;
  align-items: baseline;
}
.study__credits { font-size: .6875rem; letter-spacing: .1em; text-transform: uppercase; color: var(--pencil); }
.study__next { display: flex; align-items: baseline; gap: .8rem; }
.study__next a {
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 3vw, 2.2rem);
  font-weight: 300;
  letter-spacing: -.02em;
  color: var(--ink);
  text-decoration: none;
}
.study__next a:hover { color: var(--graphite); }

/* the empty-slot placeholder, on dark — sits behind the real photo and only
   shows through if the image is missing or fails to load. */
.slot {
  position: absolute;
  inset: 0;
  background: var(--paper-deep);
  background-image: repeating-linear-gradient(
    -45deg, transparent 0 9px, rgba(255,255,255,.03) 9px 10px);
}
.study__year { font-size: .625rem; letter-spacing: .14em; text-transform: uppercase; color: var(--pencil); }

/* ------------------------------------------------- the mascot at the hub */
/* Not a project — the thing they all belong to. Sits on transparent ground
   rather than in a photo frame, and never dims when you filter or hover. */
.link--hub { stroke-dasharray: 1 6; stroke: var(--pencil); }

.node--mark { cursor: default; }
.node--mark .node__shot {
  width: clamp(74px, 8vw, 118px);
  background: none;
  box-shadow: none;
  overflow: visible;
}
.node--mark .node__shot img { object-fit: contain; }
.node--mark .node__tag { color: var(--ink); }
.node--mark .node__name { opacity: 1; font-weight: 600; }
.node--mark .node__co { opacity: .55; }
.node--mark.is-on .node__shot { scale: 1.16; }

/* ===========================================================================
   THE SCRIBBLE
   ---------------------------------------------------------------------------
   A real hand-drawn underline that DRAWS ITSELF IN on hover and draws back
   OUT on leave — not a shape that fades or scales, an SVG path that is
   actually traced, the way the reference component does it with
   framer-motion's pathLength/pathOffset. This site has no animation library,
   so main.js reimplements exactly that with stroke-dasharray/-dashoffset,
   which is the vanilla equivalent: dashoffset length->0 draws the line in;
   continuing past 0 to -length slides the same dash off the far end, which
   is what pathOffset 0->1 does with pathLength held at 1 — an erase that
   travels start-to-end rather than a fade.
   Six squiggle variants, ported verbatim from the reference's SVG paths, and
   main.js hands out a different one on every hover the same way its
   `globalSVGState` cycles through them.
   ========================================================================= */
.masthead__nav a, .cue, .filter__b, .filter__toggle, .wordmark,
.colophon__email, .colophon__links a, .colophon__foot a,
.study__back, .study__link, .study__next a { position: relative; }
.wordmark { --swipe-colour: var(--swipe-yellow); }
.masthead__nav a:nth-child(1) { --swipe-colour: var(--swipe-yellow); }
.masthead__nav a:nth-child(2) { --swipe-colour: var(--swipe-orange); }
.masthead__nav a:nth-child(3) { --swipe-colour: var(--swipe-blue); }
.cue { --swipe-colour: var(--swipe-orange); }
.filter__toggle { --swipe-colour: var(--swipe-orange); }
.masthead__nav a:hover, .cue:hover { color: var(--ink); }

/* the traced-path underline main.js injects under each of the above */
.scribble-svg {
  position: absolute;
  left: 0; top: 100%;
  width: 100%;
  overflow: visible;
  pointer-events: none;
  color: var(--swipe-colour, var(--swipe-yellow));
}
.scribble-svg path { stroke: currentColor; fill: none; }

/* the hand-drawn hover circle main.js wraps a works-grid picture in — same
   draw-in/erase trick as .scribble-svg, just a closed loop instead of a
   strip, and positioned by main.js (it has to sit over the picture, not
   under a line of text). */
.circle-scribble-svg {
  position: absolute;
  overflow: visible;
  pointer-events: none;
  color: var(--swipe-colour, var(--swipe-orange));
}
.circle-scribble-svg path { stroke: currentColor; fill: none; }

/* the drawn wordmark */
.wordmark { display: inline-flex; align-items: center; line-height: 0; }
.wordmark__art {
  height: clamp(26px, 3.2vw, 38px);
  width: auto;
  display: block;
}

/* ------------------------------------------------- the filter, as a menu */
/* Collapsed it is one word. Open, the disciplines run out sideways from it. */
.filter {
  display: flex;
  align-items: center;
  gap: .5rem;
  justify-content: center;
  padding: 0 0 clamp(.75rem, 2vh, 1.4rem);
}
.filter__toggle {
  display: inline-flex;
  align-items: baseline;
  gap: .45rem;
  flex: none;
  font-size: .6875rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--pencil);
  padding: .3rem 0;
  transition: color .22s ease;
}
.filter__toggle.is-on { color: var(--ink); }
.filter__sign {
  width: 9px; height: 9px;
  position: relative;
}
.filter__sign::before,
.filter__sign::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto;
  height: 1.5px;
  background: currentColor;
  transition: rotate .3s cubic-bezier(.2,.8,.3,1), opacity .3s ease;
}
.filter__sign::after { rotate: 90deg; }
.filter.is-open .filter__sign::after { rotate: 0deg; opacity: 0; }

.filter__list {
  display: flex;
  align-items: center;
  gap: .1rem;
  /* overflow-x only: the width-open animation needs horizontal clipping,
     but clipping vertically too would cut off each chip's drawn underline,
     which sits below it. */
  overflow-x: hidden;
  overflow-y: visible;
  padding-bottom: .9rem;   /* room for the underline, since it can't overflow */
  margin-bottom: -.9rem;
  max-width: 0;
  opacity: 0;
  transition: max-width .55s cubic-bezier(.2,.8,.3,1), opacity .3s ease;
}
.filter.is-open .filter__list { max-width: 78vw; opacity: 1; }

.filter__b {
  flex: none;
  position: relative;
  z-index: 0;
  white-space: nowrap;
  padding: .3rem .62rem;
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--pencil);
  transition: color .22s ease;
}
.filter__b:hover, .filter__b.is-on { color: var(--ink); }
.filter__b:nth-child(4n+1) { --swipe-colour: var(--swipe-yellow); }
.filter__b:nth-child(4n+2) { --swipe-colour: var(--swipe-pink); }
.filter__b:nth-child(4n+3) { --swipe-colour: var(--swipe-orange); }
.filter__b:nth-child(4n+4) { --swipe-colour: var(--swipe-blue); }

@media (max-width: 860px) {
  .filter { flex-wrap: wrap; }
  /* the list keeps its fully-open footprint at all times here, so opening
     it never shifts anything below it — only its opacity/pointer-events
     toggle, from the exact spot it already occupies. */
  .filter__list {
    flex-wrap: wrap;
    justify-content: center;
    max-width: 100%;
    opacity: 0;
    pointer-events: none;
    transition: opacity .3s ease;
  }
  .filter.is-open .filter__list { opacity: 1; pointer-events: auto; }
}
