/* =========================================================
   VOID — this project ships with its own poster-style layout,
   rebuilt from the source layout (now in ../portfolio-sources/sources/).
   The design is drawn on a 1920 px canvas, so every dimension
   here is expressed as a fraction of that canvas (vw) and the
   whole page scales as one piece, exactly like the source.
   ========================================================= */

/* the canvas measures itself against the page box, scrollbar excluded */
/* the palette is repeated on body so the site header, which sits outside .vd,
   can read it too. The padding is declared here once and inherited by .vd, so
   there is a single place to change it. */
body:has(> .vd) {
  container-type: inline-size;
  --vd-red: #cc3131;
  --vd-cream: #e8e8e2;
  --vd-ink: #111111;
  /* the one number that sets the page's side margin */
  --vd-pad-n: 0.05;
  --vd-pad: calc(var(--vd-pad-n) * 100%);
  --vd-inset: calc(1 - 2 * var(--vd-pad-n));
  background: var(--vd-red);
}

.vd {
  --vd-red: #cc3131;
  --vd-cream: #e8e8e2;
  --vd-ink: #111111;
  /* Each slide paints its own background right out to the page edge and pads
     its content in by --vd-pad. Nothing is mounted on a contrasting margin:
     the content simply sits a step smaller inside its own colour.
     --vd-pad and --vd-inset are both derived from --vd-pad-n on the body, so
     they can no longer drift apart. */
  --vd-ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* Inter is kept in the stack because many machines have it locally, but it
     is no longer fetched as a webfont: it was a whole family downloaded to
     stand behind a Switzer that loads from the same request anyway. */
  --vd-sans: "Switzer", "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* 1 canvas unit = 1/1920 of the page width. Measured in container units, not
     vw, so the scrollbar does not throw every dimension off by ~1%. */
  --u: calc(min(100cqw, 1920px) * var(--vd-inset) / 1920);
  background: var(--vd-cream);
  color: var(--vd-ink);
  font-family: var(--vd-sans);
}
.vd img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* ---------- hero ---------- */
.vd__hero {
  position: relative;
  width: 100%;
  /* The source slide is 1920x1260, but with the foreground at 70% the artwork
     only occupied the middle 56% of that height: 17% of dead space above and
     27% below. Measured off the alpha channel of hero-scooter.png, whose real
     content runs y 15%..85.1% of its own box. Cropped to 1920x840, which leaves
     an even 8% margin top and bottom and nothing else. */
  aspect-ratio: 1920 / 840;
  overflow: hidden;
  /* The backdrop used to be a photograph of the studio sweep with the scooter
     already in it, with the cut-out laid exactly on top so only one scooter
     showed. Once the cut-out came down to 70% the one behind it appeared as a
     second, larger scooter. The sweep is now drawn instead — the stops are
     sampled off the original file: 222 at the centre, 161 at the edges, 150 in
     the corners — which kills the duplicate and saves the 66 KB image. */
  background: radial-gradient(ellipse farthest-corner at 50% 48%,
    #dedede 0%, #cfcfcf 28%, #b9b9b9 55%, #a3a3a3 78%, #969696 100%);
}
.vd__hero-scooter {
  position: absolute;
  /* heights x 1.5 to hold the same pixel size in the shorter slide */
  left: 15%; top: -10%; width: 70%; height: 120%;
}
.vd__hero-scooter img { object-fit: contain; object-position: center; }
/* the wordmark lockup and the brush script are one piece of artwork in the
   source, set in faces that aren't ours — lifted from the PDF as vector-clean
   artwork rather than re-typed in a substitute */
/* Slide one was drawn to fill a 1920 canvas edge to edge, which on a laptop
   pushes the lockup and the scooter past the top and bottom of the viewport.
   The three foreground elements are scaled to 70% about the centre of the
   slide — new = 50 + (old - 50) x 0.7 — so the composition is unchanged and
   the whole of it lands inside one screen. The backdrop plate is left alone. */
.vd__lockup {
  position: absolute;
  left: 27.36%; top: 21.9%; width: 45.24%;
  height: auto;
}
.vd__lockup img { height: auto; object-fit: contain; }
.vd__tagline {
  position: absolute;
  /* the source sets this at 72.62%, where the deck of the scooter cut-out runs
     through the last two letters of SCOOTER. Dropped to the first line that
     clears the artwork completely, so the layering is still the source's and
     the words are still readable. */
  left: 27.36%; top: 89.8%;
  margin: 0;
  font-size: calc(27.3 * var(--u));
  font-weight: 700;
  letter-spacing: calc(0.6 * var(--u));
  color: var(--vd-ink);
  white-space: nowrap;
}
/* the scooter sits above the type, exactly as it is layered in the source */

/* ---------- challenge ---------- */
.vd__challenge {
  background: var(--vd-red);
  color: var(--vd-cream);
  padding: calc(47 * var(--u)) var(--vd-pad) calc(50 * var(--u));
}
/* Display headlines. --fs is the size the design uses; --ls is tracking tuned
   per headline so the substitute face spans the same width the original does. */
/* The section headlines sat hard against the left edge, which on a wide screen
   puts them outside the band the eye actually reads. Centred instead, so every
   slide opens where the reader is already looking. */
.vd__display {
  margin: 0;
  font-size: calc(var(--fs, 288) * var(--u));
  font-weight: 800;
  line-height: 0.72;
  letter-spacing: calc(var(--ls, 0) * var(--u));
  text-align: center;
  text-transform: uppercase;
}
/* Two steps, and only two: the sheets that carry a section, and the covers
   that sit over a photograph. Every headline on the page is one or the other. */
/* 180 units was drawn for a poster read at arm's length; on a screen it filled
   the width and pushed everything else down. One step for every section
   heading on the page, so Challenge, Methodology, Simulation, Lattices and
   Parts all carry the same weight. */
.vd__challenge .vd__display,
.vd__fea .vd__display,
.vd__band .vd__display,
.vd__sheet .vd__display { --fs: 118; --ls: -1; }
/* The space under a heading used to be set on the block beneath it, and those
   blocks carry a scaled --u — so the same number produced a different gap in
   each section. It lives on the headings now, which all share the page unit,
   and Methodology, Simulation, Lattices and Parts finally match. */
.vd__fea .vd__display,
.vd__band .vd__display,
.vd__sheet .vd__display { margin-bottom: calc(140 * var(--u)); }
.vd__grid {
  /* Everything inside is expressed in --u, so redefining the unit here shrinks
     the type, the padding and the icons together and keeps the block in
     proportion — rather than nudging a dozen numbers by hand. */
  --u: calc(min(100cqw, 1920px) * var(--vd-inset) / 1920 * 0.8);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  width: 84%;
  margin: calc(218 * var(--u)) auto 0;
  border: 1px solid var(--vd-cream);
}
.vd__grid > * { padding: calc(28 * var(--u)) calc(42 * var(--u)) calc(18 * var(--u)); }
.vd__grid > * + * { border-left: 1px solid var(--vd-cream); }
.vd__icon { width: auto; height: calc(53 * var(--u)); margin-bottom: calc(34 * var(--u)); }
.vd__icon img { width: auto; height: 100%; object-fit: contain; }
.vd__grid h3 {
  margin: 0 0 calc(30 * var(--u));
  font-size: calc(40 * var(--u));
  font-weight: 700;
  line-height: 1.16;
  letter-spacing: calc(-0.4 * var(--u));
}
.vd__grid p {
  margin: 0;
  font-size: calc(24 * var(--u));
  line-height: 1.32;
}

/* ---------- four tiles ---------- */
.vd__tiles { display: grid; grid-template-columns: repeat(4, 1fr); }
.vd__tile {
  position: relative;
  /* the source draws these 480x1079, which is taller than a laptop screen —
     the note at the top and the label at the foot could never be read at the
     same time. Cropped top and bottom (the photograph is on object-fit: cover)
     so the whole tile lands inside one viewport. */
  aspect-ratio: 480 / 880;
  margin: 0;
  overflow: hidden;
  color: var(--vd-cream);
}
/* The label sat at the foot and the note at the head, a full tile height apart,
   so the two halves of one idea never read as a pair. They are now a single
   centred block: the label as the statement, the note under it as its subtitle. */
.vd__tile img { position: absolute; inset: 0; }
.vd__tile-content {
  position: relative;
  /* ::after is the element's last child, so without this the scrim would paint
     on top of the very words it exists to make readable */
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: 0 calc(34 * var(--u));
  text-align: center;
}
/* a soft darkening under the words only: the text now sits over the middle of
   the photograph, which is where the subject is */
.vd__tile::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(70% 42% at 50% 50%, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.28) 55%, transparent 100%);
  pointer-events: none;
}
.vd__tile-label {
  margin: 0 0 calc(20 * var(--u));
  font-size: calc(44 * var(--u));
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: calc(-0.5 * var(--u));
  text-transform: uppercase;
}
.vd__tile-note { margin: 0; font-size: calc(23 * var(--u)); line-height: 1.36; }
.vd__tile b { font-weight: 700; }

/* =========================================================
   PART 2 — OPTIMITZACIO / METODOLOGIA / SIMULACIO
   ========================================================= */

/* the headline is centred in the slide's height rather than dropped to its
   foot; a full-height box does the centring so `transform` stays free for the
   pointer parallax */
/* A cover headline crosses whatever the photograph happens to be doing behind
   it — the wheel shot runs from near-black to near-white under the same word.
   So it inverts what is behind it, exactly like the site header does: white
   type in difference mode, no panel, no box. The blend has to sit on the title
   wrapper rather than the h2, because `will-change: transform` on the wrapper
   opens its own stacking context and would trap the blend inside it. */
/* Difference blending fails in exactly one place: where the photograph sits at
   mid grey, white differenced against it returns the same mid grey and the
   letters vanish (measured 1.00:1; on the Optimisation plate 71% of the area
   behind the word sat under 4.5:1).
   Darkening the picture is the wrong tool — it rescues a dark plate but pushes
   a light one *into* the failure zone, which is what happened to Simulation
   (13% -> 65% when a plain scrim was tried). What works is raising contrast:
   it moves light pixels lighter and dark pixels darker, away from mid grey in
   both directions at once. Measured across the headline band, contrast(1.8)
   takes the three plates to 16.4% / 8.1% / 12.5%.
   Confined to the band and feathered at both edges, so nothing reads as a
   panel sitting behind the type. */

/* a cream section whose headline is red */
/* a band that carries nothing but a section heading, used where the slide it
   used to sit on has gone */
.vd__band {
  background: var(--vd-cream);
  padding: calc(150 * var(--u)) var(--vd-pad) 0;
}
.vd__sheet { background: var(--vd-cream); padding: calc(60 * var(--u)) var(--vd-pad) calc(15 * var(--u)); }

/* four captioned figures in a row, each in its own red-ruled frame */
.vd__four {
  /* Everything inside is expressed in --u, so redefining the unit here shrinks
     the type, the padding and the icons together and keeps the block in
     proportion — rather than nudging a dozen numbers by hand. */
  --u: calc(min(100cqw, 1920px) * var(--vd-inset) / 1920 * 0.8);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0 calc(55 * var(--u));
  width: 84%;
  margin-inline: auto;
}
.vd__four figure { margin: 0; }
.vd__four h3 {
  margin: 0 0 calc(22 * var(--u));
  min-height: calc(72 * var(--u));
  font-size: calc(32 * var(--u));
  font-weight: 700;
  line-height: 1.12;
  color: var(--vd-red);
  text-transform: uppercase;
  letter-spacing: calc(0.4 * var(--u));
}
.vd__four img { width: 100%; height: auto; }
.vd__four figcaption {
  margin-top: calc(5 * var(--u));
  font-size: calc(24 * var(--u));
  line-height: 1.32;
}
/* Lattices runs straight into the component breakdowns below it, and the
   two share a cream ground — without a gap they read as one unbroken slab */
.vd__sheet--lattices { padding-bottom: calc(150 * var(--u)); }
.vd__sheet--lattices .vd__four figcaption { margin-top: calc(20 * var(--u)); }
.vd__sheet--lattices .vd__four h3 { margin-bottom: calc(35 * var(--u)); min-height: 0; }

/* the FEA grid: two panels per row, each under its own heading */
/* Methodology -> Simulation -> Lattices: the gaps either side of the
   simulations are doubled, so each section reads as its own slide again rather
   than as one continuous cream run. */
.vd__fea { background: var(--vd-cream); padding: calc(225 * var(--u)) var(--vd-pad) calc(180 * var(--u)); }

/* =========================================================
   PART 3 — component breakdowns and the closing lockup
   ========================================================= */
/* These ran the full padded width, which threw the body text out to the right
   edge of the screen and the photograph out to the left. Brought in to 86% and
   centred, with the unit scaled to match, so the pair reads as one block near
   the middle instead of two things at opposite sides. */
.vd__part {
  --u: calc(min(100cqw, 1920px) * var(--vd-inset) / 1920 * 0.86);
  width: 86%;
  margin-inline: auto;
  padding-inline: 0;
  display: grid;
  grid-template-columns: 1000fr 920fr;
  background: var(--vd-cream);
  /* tops aligned: the text and the photograph start on the same line and, since
     both scale on --u, they stay in step as the window moves */
  align-items: start;
}
/* Every breakdown had the photograph on the left, which made five of them in a
   row read as one long column. Alternating gives the run a rhythm and keeps the
   eye moving across rather than straight down. */
/* mirrored properly: the outer padding moves to the outer side, and nothing is
   left between the text column and the photograph but the body's own inner gap */
.vd__part--rev .vd__part-figure { order: 2; padding-left: 0; padding-right: calc(40 * var(--u)); }
/* In the flipped blocks the text column sits on the outside, and because the
   callouts are capped at 560u the block ended up stranded against the far edge
   with a wide gap before the photograph. Laid out as a column whose items align
   to their own right edge: the heading, the weight and the callouts all finish
   on the centre line, hard against the image, while the words still read
   left to right. */
.vd__part--rev .vd__part-body {
  padding: calc(80 * var(--u)) calc(38 * var(--u)) 0 calc(60 * var(--u));
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}
.vd__part-figure { margin: 0; padding-left: calc(40 * var(--u)); }
/* 20% smaller, and pinned to the inner edge of its column so the space it gives
   back falls on the outside of the page rather than between the photograph and
   the words */
.vd__part-figure img { width: 80%; height: auto; margin-left: auto; }
.vd__part--rev .vd__part-figure img { margin-left: 0; margin-right: auto; }
.vd__part-body { padding: calc(80 * var(--u)) calc(60 * var(--u)) 0 calc(38 * var(--u)); }
.vd__part-body h2 {
  margin: 0;
  font-size: calc(132 * var(--u));
  font-weight: 800;
  line-height: 0.78;
  letter-spacing: calc(-1.5 * var(--u));
  color: var(--vd-red);
  text-transform: uppercase;
}
/* the weight is set in the brush face the design uses, kept as artwork */
.vd__weight { display: block; width: auto; height: calc(96 * var(--u)); margin: calc(24 * var(--u)) 0 0; }
.vd__weight img { width: auto; height: 100%; object-fit: contain; }
/* The callouts sit straight under the weight with a gap of their own. An auto
   margin was tried here and was wrong: it pushed them to the foot of the column
   and left a gap that changed size every time the window moved. Measured across
   1135-1585px, the block runs about 3% shorter than the photograph beside it at
   every width — both scale on --u — so a fixed proportional gap holds the two
   in step and the text never reaches the foot of the image. */
.vd__notes { list-style: none; margin: calc(36 * var(--u)) 0 0; padding: 0; counter-reset: vdnote; }
.vd__notes li {
  position: relative;
  counter-increment: vdnote;
  padding-left: calc(103 * var(--u));
  /* capped so the lines break into a block rather than running the full width of
     the column; a measure this length is read, a 90-character line is scanned */
  max-width: calc(560 * var(--u));
  margin-bottom: calc(26 * var(--u));
  font-size: calc(24 * var(--u));
  line-height: 1.42;
  color: var(--vd-red);
}
/* no trailing gap under the last one: it served no purpose and cost height */
.vd__notes li:last-child { margin-bottom: 0; }
.vd__notes li::before {
  content: counter(vdnote);
  position: absolute;
  left: 0;
  /* Half the difference between the paragraph's line box (1.42 x 24u) and the
     disc (56u), spelled out in units rather than em: inside ::before an em
     resolves against the pseudo-element's own font-size - the numeral at 30u -
     not the paragraph's, which left the disc sitting 2px low. */
  top: calc((1.42 * 24 * var(--u) - 56 * var(--u)) / 2);
  width: calc(56 * var(--u)); height: calc(56 * var(--u));
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--vd-red);
  color: var(--vd-cream);
  font-size: calc(30 * var(--u));
  font-weight: 700;
}
.vd__notes b { font-weight: 700; }

.vd__end {
  background: var(--vd-red);
  padding-inline: var(--vd-pad);
  aspect-ratio: 1920 / 1065;
  display: grid;
  place-items: center;
}
.vd__end img { width: 55.73%; height: auto; object-fit: contain; }

/* =========================================================
   NARROW SCREENS
   The canvas keeps scaling with the viewport at every width — pinning --u
   made the display headlines overflow — so only the copy gets a floor, and
   the four-up rows break into pairs and then a single column.
   ========================================================= */
@media (max-width: 1100px) {
  .vd__grid, .vd__tiles, .vd__four { grid-template-columns: repeat(2, 1fr); }
  .vd__grid > *:nth-child(3) { border-left: 0; }
  .vd__grid > *:nth-child(n+3) { border-top: 1px solid var(--vd-cream); }
  .vd__four { gap: calc(90 * var(--u)) calc(55 * var(--u)); }
  /* two-up, so each tile is twice as wide and would tower again */
  .vd__tile { aspect-ratio: 480 / 620; }
  .vd__part { grid-template-columns: 1fr; }
  .vd__part-figure { padding: 0 calc(40 * var(--u)); }
  .vd__part-body { padding: calc(60 * var(--u)) calc(56 * var(--u)); }
  .vd__notes { margin-top: calc(60 * var(--u)); }
  /* stacked on a phone, so there is no photograph to sit inside */
  .vd__part { align-items: start; }

  .vd__grid p, .vd__four figcaption, .vd__tile-note, .vd__notes li { font-size: max(calc(24 * var(--u)), 15px); }
  .vd__grid h3 { font-size: max(calc(40 * var(--u)), 20px); }
  .vd__four h3 { font-size: max(calc(32 * var(--u)), 15px); }
  .vd__tile-label { font-size: max(calc(44 * var(--u)), 21px); }
  .vd__tile-note { font-size: max(calc(23 * var(--u)), 14px); }
  .vd__tagline { font-size: max(calc(39 * var(--u)), 14px); }
  .vd__part-body h2 { font-size: max(calc(132 * var(--u)), 46px); }
  .vd__notes li::before { width: max(calc(56 * var(--u)), 30px); height: max(calc(56 * var(--u)), 30px); font-size: max(calc(30 * var(--u)), 16px); }
  /* the disc and the copy both hit floors here, so the centring has to follow */
  .vd__notes li::before { top: calc((1.42 * max(calc(24 * var(--u)), 15px) - max(calc(56 * var(--u)), 30px)) / 2); }
  .vd__notes li { padding-left: max(calc(103 * var(--u)), 46px); }
  /* the 560u cap is there to shorten the measure on a wide screen; on a narrow
     one it collapses to 85px, below the 46px number gutter, and the text spills
     out of the page. The column is already narrow here, so the cap comes off. */
  .vd__notes li { max-width: none; }
  /* stacked, so there is no image to hug: the flipped blocks read normally */
  .vd__part--rev .vd__part-body { display: block; }
  .vd__part--rev .vd__part-figure { padding-left: 0; padding-right: 0; }
}
@media (max-width: 640px) {
  .vd__grid, .vd__tiles, .vd__four { grid-template-columns: 1fr; }
  .vd__grid > * + * { border-left: 0; border-top: 1px solid var(--vd-cream); }
  .vd__grid > *:nth-child(3) { border-left: 0; }
  .vd__tile { aspect-ratio: 4 / 5; }
  .vd__four h3 { min-height: 0; }
}

/* =========================================================
   The page ends on the brand red, so the portfolio's own
   prev/next bar and contact footer carry it through instead
   of dropping back to white.
   ========================================================= */
body:has(> .vd) .proj__nav,
body:has(> .vd) .foot {
  background: var(--vd-red);
  color: var(--vd-cream);
  border-top-color: rgba(232, 232, 226, 0.25);
}
body:has(> .vd) .proj__nav a,
body:has(> .vd) .foot__label,
body:has(> .vd) .foot__row { color: rgba(232, 232, 226, 0.72); }
body:has(> .vd) .proj__nav a:hover,
body:has(> .vd) .proj__nav .proj__next,
body:has(> .vd) .foot__email,
body:has(> .vd) .foot__links a:hover { color: var(--vd-cream); }
body:has(> .vd) .proj__nav,
body:has(> .vd) .foot { max-width: none; margin-inline: 0; padding-inline: var(--vd-pad); }
body:has(> .vd) .foot__email { display: block; }

/* =========================================================
   MOTION
   A poster pasted into a browser reads as a PDF, so the page
   earns a little movement: the hero assembles itself, sections
   arrive as they are scrolled to, and the things you can point
   at answer. `.vd-motion` is only set when the visitor has not
   asked for reduced motion, so every rule here is opt-in.
   ========================================================= */
@keyframes vd-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes vd-rise { from { opacity: 0; transform: translateY(2.5%); } to { opacity: 1; transform: none; } }

/* the hero assembles: ground, wordmark, scooter, strapline */
.vd-motion .vd__lockup { animation: vd-rise 1s var(--vd-ease) 0.15s both; }
.vd-motion .vd__hero-scooter { animation: vd-rise 1.1s var(--vd-ease) 0.3s both; }
/* Each layer answers the pointer by a different amount, which is what reads as
   depth. The lerp lives in main.js; --mx/--my are a fraction of the hero box. */
.vd__hero { --mx: 0; --my: 0; }
.vd-motion .vd__lockup img { transform: translate(calc(var(--mx) * -1.5%), calc(var(--my) * -1.5%)); }
.vd-motion .vd__hero-scooter img { transform: translate(calc(var(--mx) * 3.4%), calc(var(--my) * 3.4%)); }
.vd-motion .vd__tagline { animation: vd-fade 0.8s var(--vd-ease) 0.55s both; }

/* sections arrive on scroll */
/* :where() keeps this at the specificity of .vd-motion alone — spelling the
   selectors out plainly made `.vd__four figure` and `.vd__end img` out-rank
   `.is-shown`, and those sections never came back. */
.vd-motion :where(.vd__sheet .vd__display, .vd__challenge .vd__display,
                  .vd__grid > *, .vd__tile, .vd__four figure) {
  opacity: 0;
  transform: translateY(calc(34 * var(--u)));
}
/* the component blocks grow into place rather than sliding; the numbered
   callouts stay put, the title carries the movement */
/* the component photographs stay still; only the wording grows in */
.vd-motion :where(.vd__part-body h2, .vd__weight) {
  opacity: 0;
  transform: scale(0.94);
}
.vd-motion :where(.vd__part-body h2, .vd__weight) { transform-origin: left center; }
/* The closing lockup leans towards the pointer and ripples under it. The
   displacement filter is always attached (at scale 0 it costs nothing) so
   there is no re-render pop the moment the cursor arrives; main.js ramps the
   scale on hover. */
.vd__end { --mx: 0; --my: 0; }
.vd__end img {
  transform: translate(calc(var(--mx) * 4%), calc(var(--my) * 4%));
  filter: url(#distort);
}
.vd-motion .is-shown {
  opacity: 1;
  transform: none;
  transition: opacity 0.8s var(--vd-ease), transform 0.8s var(--vd-ease);
}
/* once revealed, these hand their transform back to the hover, which wants a
   quicker curve than the arrival does */
.vd-motion .vd__four figure.is-shown {
  transition: opacity 0.8s var(--vd-ease), transform 0.45s var(--vd-ease);
}
.vd-motion .vd__grid > .is-shown {
  transition: opacity 0.8s var(--vd-ease), transform 0.45s var(--vd-ease),
              background-color 0.45s var(--vd-ease);
}

/* the photographs drift a little as they pass, where the browser can do it
   off the scroller without script */
/* The covers get their depth from the pointer, the same way the hero does:
   the photograph leans one way, the headline the other and further, so the
   title sits in front of a picture that has fallen away behind it. */
.vd-motion .vd-motion 
/* ---- hovers: the frame holds, the content answers ---- */
/* the zoom was jumping at the reader rather than answering them; halved and
   slowed, so it reads as the tile acknowledging the cursor */
.vd__tile img { transition: transform 1.1s var(--vd-ease); }
.vd__tile:hover img { transform: scale(1.022); }
.vd__tile-note, .vd__tile-label { transition: transform 0.7s var(--vd-ease); }
.vd__tile:hover .vd__tile-label { transform: translateY(calc(-5 * var(--u))); }

/* the red-ruled plates lift off the sheet */
.vd__four figure, .vd__part-figure { transition: transform 0.45s var(--vd-ease); }
.vd__four figure:hover, .vd__part-figure:hover { transform: translateY(calc(-14 * var(--u))); }

/* challenge columns */
.vd__grid > * { transition: background-color 0.45s var(--vd-ease); }
.vd__grid > *:hover { background-color: rgba(0, 0, 0, 0.09); }
.vd__icon { transition: transform 0.55s var(--vd-ease); }
.vd__grid > *:hover .vd__icon { transform: translateY(calc(-8 * var(--u))) rotate(-5deg); }

/* the numbered callouts */
.vd__notes li::before { transition: transform 0.35s var(--vd-ease); }
.vd__notes li:hover::before { transform: scale(1.15); }

@media (prefers-reduced-motion: reduce) {
  .vd *, .vd *::before, .vd *::after { animation: none !important; transition: none !important; }
}

/* The simulations are grouped by the part they test and paired the way the
   source lays them out — displacement beside Von Mises. Each plate sits on the
   dark ground so the pale FEA sheets read as mounted, and opens in the shared
   viewer with its own caption. */
.vd__simgroup { margin-bottom: calc(64 * var(--u)); }
.vd__simgroup:last-child { margin-bottom: 0; }
.vd__simgroup h3 {
  text-align: center;
  margin: 0 0 calc(20 * var(--u));
  font-size: calc(32 * var(--u));
  font-weight: 700;
  letter-spacing: calc(0.4 * var(--u));
  text-transform: uppercase;
  color: var(--vd-red);
}
/* the plates come in to 74% of the column: at full width the pair straddled the
   whole screen and the eye had to travel from one edge to the other to compare
   the displacement plot against the stress plot */
.vd__simrow {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: calc(22 * var(--u));
  width: 74%;
  margin-inline: auto;
}
.vd__sim {
  position: relative;
  margin: 0;
  overflow: hidden;
  cursor: zoom-in;
  /* a hairline, not a mount: the twelve plates are cut to one common size so
     they already sit square with each other */
  border: 1px solid var(--vd-ink);
}
.vd__sim img { width: 100%; height: auto; transition: transform 0.7s var(--vd-ease); }
.vd__sim:hover img, .vd__sim:focus-visible img { transform: scale(1.04); }
.vd__sim:focus-visible { outline: 2px solid var(--vd-red); outline-offset: 3px; }

/* the renders the project closes on */
.vd__media { background: var(--vd-ink); padding: calc(80 * var(--u)) var(--vd-pad) calc(96 * var(--u)); }
.vd__media-label {
  margin: 0 0 calc(34 * var(--u));
  font-size: calc(32 * var(--u));
  font-weight: 700;
  letter-spacing: calc(0.4 * var(--u));
  text-transform: uppercase;
  color: var(--vd-cream);
}
.vd__mediagrid { columns: 3; column-gap: calc(20 * var(--u)); }
.vd__mediagrid figure {
  break-inside: avoid;
  margin: 0 0 calc(20 * var(--u));
  overflow: hidden;
  cursor: zoom-in;
  background: #000;
}
.vd__mediagrid img { width: 100%; height: auto; transition: transform 0.8s var(--vd-ease); }
.vd__mediagrid figure:hover img, .vd__mediagrid figure:focus-visible img { transform: scale(1.05); }
.vd__mediagrid figure:focus-visible { outline: 2px solid var(--vd-red); outline-offset: 3px; }
@media (max-width: 1100px) { .vd__mediagrid { columns: 2; } }
@media (max-width: 640px) {
  .vd__simrow { grid-template-columns: 1fr; }
  .vd__mediagrid { columns: 1; }
}

/* =========================================================
   NARROW SCREENS, PART TWO
   The floor block up in NARROW SCREENS only reaches rules declared above it.
   The simulation groups and the closing gallery are declared further down, so
   at equal specificity their unfloored sizes won and both headings fell to
   5.6px on a phone. Floored here, after everything they override.
   ========================================================= */
@media (max-width: 1100px) {
  .vd__simgroup h3, .vd__media-label { font-size: max(calc(32 * var(--u)), 17px); }
  .vd__simgroup h3 { margin-bottom: max(calc(20 * var(--u)), 10px); }
  .vd__media-label { margin-bottom: max(calc(34 * var(--u)), 16px); }
}

/* =========================================================
   THE SITE HEADER OVER THIS PAGE
   Difference blending reads correctly on the other seven pages, but over this
   project's red it resolves to |255-204|,|255-49|,|255-49| = #33CECE — a cyan
   that appears nowhere in the palette, and at 0.65 opacity lands near 2.4:1.
   No single blended colour can serve both the red and the light cream sheets,
   so on Void the blend comes off and the header takes its cue from whichever
   section is passing beneath it (see voidHeader() in main.js).
   ========================================================= */
body:has(> .vd) .head {
  mix-blend-mode: normal;
  /* white rather than the cream: on #cc3131 the cream lands at 4.21:1, just
     under AA, and this red leaves almost no headroom — only a near-white
     clears 4.5. White reads at 4.84 and is indistinguishable here. */
  color: #ffffff;
  transition: transform 0.4s var(--ease), color 0.25s var(--ease);
}
body:has(> .vd) .head--on-light { color: var(--vd-ink); }
/* dimming the inactive links would put them back under AA on the red, so on
   this page they stay at full strength and weight marks the active one */
body:has(> .vd) .head__nav a { opacity: 1; }
body:has(> .vd) .head__nav a.is-active { font-weight: 600; }

/* =========================================================
   TITLE BLOCK
   Every other project opens with a code, a name, a one-line tagline and a facts
   row; Void dropped the reader straight into the poster, with no h1 anywhere on
   the page and no way back out of 10,500px of scroll. Same structure as the
   others, drawn in this project's own red and cream rather than the site's
   black on white, so the poster still starts the moment you scroll.
   ========================================================= */
.vd__head {
  background: var(--vd-red);
  color: var(--vd-cream);
  padding: clamp(4.5rem, 7vh, 6rem) var(--vd-pad) clamp(2.4rem, 5vh, 3.4rem);
}
.vd__back {
  color: var(--vd-cream); opacity: 0.75; display: inline-block;
  margin-bottom: clamp(2rem, 7vh, 4.5rem);
  padding: 0.4rem 0.5rem 0.4rem 0;
  transition: opacity 0.2s var(--vd-ease);
}
.vd__back:hover { opacity: 1; }
.vd__eyebrow { display: block; margin-bottom: 1.1rem; opacity: 0.8; }
.vd__title {
  margin: 0;
  font-size: clamp(2.6rem, 8vw, 6.5rem);
  font-weight: 800;
  line-height: 0.92;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}
.vd__lead {
  margin: 1.6rem 0 0;
  font-size: clamp(1.2rem, 2.6vw, 2rem);
  font-weight: 500;
  line-height: 1.18;
  letter-spacing: -0.02em;
  max-width: 26ch;
}
.vd__facts {
  display: flex; flex-wrap: wrap; gap: 1.2rem 2.6rem;
  margin-top: clamp(1.8rem, 4vh, 2.6rem);
  padding-top: 1.4rem;
  border-top: 1px solid rgba(232, 232, 226, 0.35);
}
.vd__facts > div { display: flex; flex-direction: column; gap: 0.4rem; }
.vd__facts span { opacity: 0.7; }
.vd__facts b {
  font-family: var(--vd-sans); font-weight: 500; font-size: 0.98rem;
  letter-spacing: -0.01em; text-transform: none;
}

/* VOID-08: these read as capitals by design, but they were typed that way in
   the markup — which some screen readers spell out letter by letter, and which
   means the case can never be changed from here. Written in title case now,
   uppercased in CSS like every other label on the site. */
.vd__four h3 { text-transform: uppercase; }

/* The load case above each set of results. A simulation picture on its own says
   nothing about what was asked of the part — this is the sentence that makes
   the plates readable, so it sits between the heading and the images. */
.vd__simnote {
  text-align: center;
  margin: 0 auto calc(26 * var(--u));
  max-width: 74ch;
  font-size: calc(24 * var(--u));
  line-height: 1.5;
  color: var(--vd-ink);
}
.vd__simnote b { font-weight: 700; color: var(--vd-red); }
@media (max-width: 1100px) {
  .vd__simnote { font-size: max(calc(24 * var(--u)), 15px); margin-bottom: 14px; }
}
