/* ==========================================================================
   Portfolio — Phase 1 structural prototype
   Recreation of the reference site's architecture, grid and interaction logic.
   All values are derived from measurements of the reference at 1440×900.
   Phase 2: swap the tokens below + the content in assets/js/projects.js
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Typeface — PP Monument Extended, self-hosted from assets/fonts/
   Six styles supplied: Light (300), Regular (400), Black (900), each with a
   true italic. There are no 500 or 700 files, so nothing here asks for them —
   `font-synthesis: none` keeps the browser from faking a weight or a slant.
   Each face ships as .woff (compressed, tried first) and .otf (fallback).
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "PP Monument Extended";
  src: url("../fonts/PPMonumentExtended-Light.woff") format("woff"),
       url("../fonts/PPMonumentExtended-Light.otf")  format("opentype");
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "PP Monument Extended";
  src: url("../fonts/PPMonumentExtended-LightItalic.woff") format("woff"),
       url("../fonts/PPMonumentExtended-LightItalic.otf")  format("opentype");
  font-weight: 300; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "PP Monument Extended";
  src: url("../fonts/PPMonumentExtended-Regular.woff") format("woff"),
       url("../fonts/PPMonumentExtended-Regular.otf")  format("opentype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "PP Monument Extended";
  src: url("../fonts/PPMonumentExtended-RegularItalic.woff") format("woff"),
       url("../fonts/PPMonumentExtended-RegularItalic.otf")  format("opentype");
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "PP Monument Extended";
  src: url("../fonts/PPMonumentExtended-Black.woff") format("woff"),
       url("../fonts/PPMonumentExtended-Black.otf")  format("opentype");
  font-weight: 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "PP Monument Extended";
  src: url("../fonts/PPMonumentExtended-BlackItalic.woff") format("woff"),
       url("../fonts/PPMonumentExtended-BlackItalic.otf")  format("opentype");
  font-weight: 900; font-style: italic; font-display: swap;
}

/* --------------------------------------------------------------------------
   1. Design tokens
   -------------------------------------------------------------------------- */
:root {
  /* colour */
  --bg:            #ffffff;   /* page canvas */
  --fg:            #000000;   /* primary text */
  --muted:         #c2c2c2;   /* inactive index items */
  --overlay:       rgba(0,0,0,.8);
  --panel:         #ffffff;
  --deco:          var(--bg); /* decorative blocks — invisible in the reference */

  /* type — self-hosted PP Monument Extended (see §0) */
  --font-sans: "PP Monument Extended", "Helvetica Neue", Helvetica, Arial, sans-serif;

  --fs-nav:   15.3px;   /* header + contact block */
  --fs-body:  15.3px;
  --fs-index: 14.3px;   /* project index list */
  --fs-meta:  13.3px;   /* project meta / description column */
  --fs-mark:  19px;     /* home word-mark */

  --lh-tight: 1.24;
  --lh-meta:  1.24;
  --lh-bio:   1.32;

  /* space */
  --margin:     24px;   /* outer margin — ALL four viewport edges derive from this */
  /* the nav sits inside this band, so the band grew with --margin to keep the
     nav-to-content gap (48px) exactly as it was */
  --header-h:   57px;   /* header band height */
  --top-pad:    35px;   /* header → first content row */
  --gap-media:  20px;   /* vertical gap between media items */
  /* the rendered width of the word "projects" in the top navigation at
     --fs-nav. The nav item is centred with translateX(-50%), so its LEFT edge
     sits at `50% of the content width - half this value` — the About page
     reuses that same expression to put the services list on the same line. */
  --nav-projects-w: 95px;
  /* The About page's portrait-to-services gap, taken from the page itself:
     it is the VISIBLE vertical distance between the bottom of the ink in
     "Greta Emilia Loi" and the top edge of the photograph, measured at 2x
     from a real render (ink bottom 38.00, portrait top 92.00 = 54.00px).
     --about-gap is the track width; --about-bearing compensates for the
     blank the first glyph carries on its left, so the VISIBLE gap — image
     edge to first stroke — is the same 54.00px. */
  --about-gap: calc(54px - var(--about-bearing));
  --about-bearing: 1px;   /* measured: the first stroke sits 1px inside the box */
  /* The block is anchored by its border box, but the last line's ink sits 5px
     above that edge (the line box's descender space). Subtracting those 5px
     makes the VISIBLE gap below the text equal the visible gap at the left,
     and tying it to --margin keeps the two equal at every breakpoint. */
  --contact-b:  calc(var(--margin) - 4px);   /* visible bottom gap = visible left gap */

  /* motion */
  --t-link: .3s ease-in-out;
  --t-page: 320ms cubic-bezier(.33,1,.68,1);
}

/* --------------------------------------------------------------------------
   2. Reset / base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: 400;
  font-synthesis: none;
  -webkit-font-synthesis: none;
  line-height: var(--lh-tight);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

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

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--t-link), opacity var(--t-link);
}

p, figure { margin: 0; }
h1, h2, h3 { margin: 0; font-weight: 400; font-size: inherit; }
ul { margin: 0; padding: 0; list-style: none; }
button, input, select, textarea { font-family: inherit; font-size: inherit; line-height: inherit; }
button { color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }

.u-visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --------------------------------------------------------------------------
   3. Page shell + page transition
   -------------------------------------------------------------------------- */
.page {
  position: relative;
  min-height: 100vh;
  opacity: 0;
  transition: opacity var(--t-page);
}
.page.is-ready   { opacity: 1; }
.page.is-leaving { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .page { transition: none; opacity: 1; }
}

/* --------------------------------------------------------------------------
   4. Header
   -------------------------------------------------------------------------- */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--header-h);
  z-index: 200;
  background: var(--bg);          /* masks content scrolling up behind the nav */
  pointer-events: none;           /* only the links are clickable */
  display: grid;
  align-items: start;
  padding: var(--margin) var(--margin) 0;   /* top edge on the same token as the sides */
}

.site-header__inner {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 19px;
}

.site-header a { pointer-events: auto; font-size: var(--fs-nav); }
/* The nav items do NOT dim on hover: colour and opacity stay exactly as they
   are at rest, and the italic below is the whole of the hover response. */
/* Hover swaps the three nav items to the real Italic cut of the same family —
   PP Monument Extended RegularItalic is already loaded at weight 400, so this
   is a true italic, not a synthesised slant. Family, size, weight and letter
   spacing are untouched, and font-style is not animatable, so leaving the item
   returns it to Regular immediately. Each nav item is anchored independently
   (left edge / viewport centre / right edge), so the width difference between
   the cuts can never push a neighbour. Scoped to .site-header: the index,
   descriptions and contact rows are unaffected. */
.site-header a:hover { font-style: italic; }

/* The word-mark carries both spellings; the breakpoint decides which one is
   in the flow. Desktop is the full name, exactly as before. */
.nav__name-short { display: none; }

/* The phone's menu button and the sheet it opens. Both are off on desktop,
   where the three words stay in the bar. */
.nav__burger { display: none; }
.mobile-menu { display: none; }

/* "projects" sits on the optical centre of the viewport, not of the flex row */
.nav__center {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

/* playground + about travel together on the right. The group carries the
   auto margin, so `about` still ends exactly on the right margin and
   `projects` keeps the viewport centre it is tuned to. */
.nav__end {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--margin);        /* the site's one spacing unit */
}

/* --------------------------------------------------------------------------
   5. Contact block (viewport-pinned, bottom left)
   -------------------------------------------------------------------------- */
.contact {
  position: fixed;
  left: var(--margin);
  bottom: var(--contact-b);
  z-index: 150;
  display: grid;
  grid-template-columns: 215px auto;   /* 18px → 233px on a 1440 canvas */
  column-gap: 0;
  row-gap: 47px;
  font-size: var(--fs-nav);
}

.contact__group { display: block; }
.contact__label { margin-bottom: 12px; }   /* label sits closer to its value */
a.contact__value:hover,
.meta__link:hover { opacity: .45; }   /* only the linked rows react */

/* --------------------------------------------------------------------------
   6. Home
   -------------------------------------------------------------------------- */
.home {
  position: relative;
  min-height: 139vh;               /* reference page height 1251 @ 900 viewport */
}

.home__mark {
  position: absolute;
  top: 27.66%;   /* 346px on the reference 1251px canvas */
  left: 41%;
  font-size: var(--fs-mark);
  font-weight: 500;
  line-height: 1.3;
  color: var(--deco);              /* reference sets this to the page colour */
}

.home__deco {
  position: absolute;
  background: var(--deco);
  pointer-events: none;
}
.home__deco--a { top: -20px; left: 45.7%;  width: 132px; height: 95px; }
.home__deco--b { top: 0;     left: 93.54%; width: 90px;  height: 90px; }

/* the cursor trail lives below the header band, exactly as in the reference */
.trail-area {
  position: absolute;
  inset: 62px 0 0 0;
  pointer-events: none;
  z-index: 100;
}

/* --------------------------------------------------------------------------
   7. Project view
   3 content columns + a gutter, proportional to the reference:
   index 18→234 · meta 234→501 · gutter 501→745 · media 745→1422
   -------------------------------------------------------------------------- */
.project {
  display: grid;
  grid-template-columns: 15.385fr 19.017fr 17.379fr 48.219fr;
  padding: calc(var(--header-h) + var(--top-pad)) var(--margin) 0;
  min-height: 100vh;
}

/* --- column 1: project index -------------------------------------------- */
.index {
  grid-column: 1;
  position: sticky;
  top: calc(var(--header-h) + var(--top-pad));
  align-self: start;
  font-size: var(--fs-index);
}

.index__link {
  display: block;
  line-height: 19.5px;
  color: var(--muted);
}
.index__link:hover,
.index__link.is-active { color: var(--fg); }

/* --- column 2: meta + description ---------------------------------------- */
.meta {
  grid-column: 2;
  position: sticky;
  top: calc(var(--header-h) + var(--top-pad));
  align-self: start;
  font-size: var(--fs-meta);
  line-height: var(--lh-meta);
  max-width: 267px;
  transition: opacity 220ms ease;
}
.meta.is-swapping { opacity: 0; }

.meta__title { margin-bottom: 33px; }
/* A description may be written into two containers so the phone can lay it out
   as two real columns. Everywhere else they must not exist as boxes: the
   paragraphs stay one run in one column, exactly as before. */
.meta__desc-col { display: contents; }
/* A split description may carry an opening sentence up into the first column
   on the phone. Both readings sit in the markup: `desc-m` is the phone's pair
   of pieces, `desc-d` the whole paragraph everywhere else. Desktop shows only
   the latter, so its copy is unchanged. */
.desc-m { display: none; }
.meta__desc p + p { margin-top: 16.5px; }
/* ...but the sibling selectors above cannot see across a wrapper boundary, so
   the blank line between the last paragraph of one container and the first of
   the next is restored explicitly. The two margins collapse into one, exactly
   as they do between two plain paragraphs. */
.meta__desc-col + .meta__desc-col > p:first-child { margin-top: 16.5px; }
.meta__desc p { margin-bottom: 16.5px; }   /* reference leaves one blank line */

/* An ordinary line of the information column, not a control: it inherits the
   column's family, size, leading and colour, and the global `a` rule already
   strips the underline. 33px above it is the same step `.meta__title` uses to
   separate itself from the description — far enough to read as its own line,
   close enough to stay part of the same block. */
.meta__link { display: block; margin-top: 33px; }

/* --- column 4: media ------------------------------------------------------ */
.media {
  grid-column: 4;
  display: flex;
  flex-direction: column;
  gap: var(--gap-media);
  /* This padding IS the page's maximum scroll overshoot: the column is what
     makes a project page tall, so whatever sits below the last media item is
     exactly how far past it the page can still be scrolled. At 120px the last
     image stopped ~95px above the fixed description's baseline. Tying it to
     --margin lands the last item's bottom edge on the same global bottom
     margin every other edge on the site uses, so it comes to rest level with
     the description — and it scales with the project, since the value is a
     trailing gap rather than anything tied to a particular media count. */
  padding-bottom: var(--margin);
  transition: opacity 220ms ease;
}
.media.is-swapping { opacity: 0; }

.media__item { position: relative; width: 100%; }

.media__item img,
.media__item video {
  width: 100%;
  height: auto;
  opacity: 0;
  transition: opacity .45s ease;
}
.media__item img.is-loaded,
.media__item video.is-loaded { opacity: 1; }

/* A media slot whose media is capped to the viewport height, so a tall
   portrait image can be taken in at a glance without scrolling. The image
   keeps its own proportions and is centred in the column; the slot's height
   follows the image. Scoped to slots carrying the modifier — no global
   media rule is changed. */
.media__item--fitv { display: flex; justify-content: center; }
.media__item--fitv img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 82vh;
}

/* Same viewport-fit, but pinned to the column's RIGHT edge instead of centred,
   so the right edge stays on the grid while the left edge moves inward as the
   image narrows. Opt-in via `align: 'right'` — a slot without the flag keeps
   the centred behaviour, so nothing else is touched. */
.media__item--fitv-right { justify-content: flex-end; }

/* Same viewport-fit, but for a source whose own pixel size is smaller than
   the cap: `height` (not `max-height`) lets it scale UP to the same presence
   as its neighbours. max-width still keeps it inside the column, and the
   intrinsic ratio is preserved in both directions. */
.media__item--fitv-up img {
  /* Only the WIDTH is set — height follows from the file's own proportions, so
     this can never stretch. The width is the smaller of the column and what the
     82vh cap allows for this image's ratio, which is supplied per slot as
     --nat-w / --nat-h. (Setting an explicit height here instead would let
     max-width clamp the width without recomputing the height — that distorts.) */
  width: min(100%, calc(82vh * var(--nat-w) / var(--nat-h)));
  height: auto;
  max-width: 100%;
  max-height: none;
}

/* A media slot whose size is fixed by the tile, not by the media inside it.
   Used by Project 01 / Media 05: the slot keeps the exact dimensions the
   placeholder gave it and the video fills that box. Scoped to slots carrying
   the modifier — no global media rule is changed. */
.media__item--tile { aspect-ratio: var(--tile-ratio); overflow: hidden; }
.media__item.media__item--tile video {
  /* absolutely positioned so the video is OUT OF FLOW: it can never grow the
     slot to its own intrinsic height. The slot's size comes from the tile
     ratio alone; the video fills that box. */
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --------------------------------------------------------------------------
   8. About
   4 columns: 18→304 · 304→589 · 589→872 · 872→1422
   -------------------------------------------------------------------------- */
.about {
  display: grid;
  /* Column 1 is the portrait track, unchanged: 20.37fr of the content box and
     calc(100% * .2037) are the same width, so the photograph keeps the exact
     size it has always had at every viewport.
     Columns 2-4 are no longer proportional — they are derived from the points
     the page now aligns to. Column 2 ends on the LEFT EDGE of the "projects"
     nav item, so the services list starts on that same vertical line: measured
     from this content box that edge is .5(100% + 2*margin) - w/2 - margin,
     which reduces to 50% - w/2. Column 3 is exactly as wide as the longest
     service, so the statement column begins just after the list instead of at
     a fixed proportion. */
  grid-template-columns:
    min(265px, 20.37%)        /* portrait — the track hugs the photograph   */
    var(--about-gap)          /* measured gap, see the token                */
    max-content               /* services list, at its natural width        */
    1fr                       /* air                                        */
    calc(50% - 193.5px);      /* about text — frozen at the position it
                                 already had in this narrow-window layout,
                                 so retiring the label and shortening the
                                 service names cannot shift it             */
  padding: calc(var(--header-h) + var(--top-pad)) var(--margin) 0;
  min-height: 100vh;
}

/* every item carries an explicit row: the label shares the portrait's cell, and
   without a stated row the auto-placement cursor would push the portrait — and
   everything after it — onto a second row. */
.about__portrait { grid-column: 1; grid-row: 1; }
/* The portrait box keeps the 265 x 336 proportion it has always had; the
   photograph (1122 x 1402) is a fraction wider, so `cover` trims ~2px from each
   side and nothing from the top or bottom. Centred — the face sits mid-frame. */
.about__portrait img {
  width: 265px;
  max-width: 100%;
  height: auto;
  aspect-ratio: 265 / 336;
  object-fit: cover;
  object-position: 50% 50%;
}

.about__services { grid-column: 3; grid-row: 1; font-size: var(--fs-nav); line-height: 18.5px; }

/* The About page carries two versions of the same statement: the full text,
   and a shorter one written for the phone. Exactly one of them is ever in the
   flow, so the grid only sees a single .about__statement and every rule that
   places or styles the block keeps working untouched. The short one is out
   everywhere by default and only steps in below the mobile breakpoint. */
.about__statement--short { display: none; }

.about__statement {
  grid-column: 5;
  grid-row: 1;
  font-size: var(--fs-body);   /* body size, not a display size */
  line-height: var(--lh-bio);
  max-width: 550px;
  padding-bottom: 120px;
}

/* One blank line between paragraphs — the same convention the project
   descriptions use, expressed in this block's own size and leading so the gap
   always equals exactly one line of its text. */
.about__statement p + p { margin-top: calc(var(--fs-body) * var(--lh-bio)); }

/* Wide layout: three bands — portrait, services, about text.
   The about text starts on the LEFT EDGE of the "projects" nav item. That item
   is centred with translateX(-50%), so its left edge is `50% of the content
   width - half the word`; giving the last track the complementary width,
   `50% + half the word`, makes the track BEGIN exactly on that line.
   Everything left of it is: the photograph's own width, then the two services
   blocks at their natural widths, with the leftover split between three equal
   1fr tracks — so the services pair sits in the middle area with the same air
   on either side of it.
   The floor: below this width the label and the list (99px + 217px of text)
   no longer fit between the photograph and the nav line at all, so the
   previous arrangement is kept there rather than letting them collide. */
@media (min-width: 1377px) {
  .about {
    grid-template-columns:
      min(265px, 20.37%)      /* portrait — the track hugs the photograph */
      var(--about-gap)        /* measured gap, see the token              */
      max-content             /* the services list                        */
      1fr                     /* air                                      */
      calc(50% + var(--nav-projects-w) / 2);  /* about text, from the line */
  }
  .about__services       { grid-column: 3; }
  /* The text sits at the END of its track instead of the start: the track still
     runs from the "projects" line to the right margin, so nothing before it
     moves, but the block's own right edge now lands on the global right margin
     — under the "about" nav item, which is anchored to that same edge. Width,
     measure and line breaks are untouched; only the box's offset changes, and
     the text inside stays left-aligned. */
  .about__statement      { grid-column: 5; justify-self: end; }
}

/* --------------------------------------------------------------------------
   9. Drawer menu (present in the reference markup; used below 1000px)
   -------------------------------------------------------------------------- */
.drawer-overlay {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  opacity: 0;
  visibility: hidden;
  transition: opacity .35s ease, visibility .35s;
  z-index: 300;
}

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  width: 300px;
  max-width: 84vw;
  height: 100%;
  background: var(--panel);
  transform: translateX(100%);
  /* `visibility` is the fallback for a browser without `inert`: it takes the
     closed drawer out of the tab order too. It flips to hidden only once the
     slide-out has finished, so the animation is exactly the one it always was;
     the overlay above already uses the same pattern. */
  visibility: hidden;
  transition: transform .35s cubic-bezier(.33,1,.68,1),
              visibility 0s linear .35s;
  z-index: 310;
  padding: 100px 40px 40px 60px;
  overflow-y: auto;
}

.drawer__close {
  position: absolute;
  top: 40px;
  right: 40px;
  width: 20px;
  height: 20px;
}
.drawer__close svg { width: 100%; height: 100%; }

.drawer__list { font-size: var(--fs-nav); }
.drawer__list li + li { margin-top: 14px; }
.drawer__list a { color: var(--muted); }
.drawer__list a:hover,
.drawer__list a.is-active { color: var(--fg); }
.drawer__sep { margin: 26px 0; height: 1px; background: #e2e2e2; }

body.is-drawer-open .drawer-overlay { opacity: 1; visibility: visible; }
body.is-drawer-open .drawer {
  transform: translateX(0);
  visibility: visible;
  transition: transform .35s cubic-bezier(.33,1,.68,1),
              visibility 0s;
}
body.is-drawer-open { overflow: hidden; }

/* --------------------------------------------------------------------------
   10. Cursor image trail (shadow-DOM host)
   -------------------------------------------------------------------------- */
image-trail { display: block; width: 100%; height: 100%; pointer-events: none; }

/* --------------------------------------------------------------------------
   11. Playground
   An archive rather than a grid. Explicit columns (not CSS multicol) let an
   item be appended to whichever column is currently shortest, so the page
   stays balanced and nothing already on screen moves when a new batch of
   media arrives. Each item keeps its own proportions and simply stacks, so
   nothing is cropped and no slot has to be told how tall it is. Items settle
   into place as they enter the viewport.
   -------------------------------------------------------------------------- */
.playground {
  --pg-cols: 4;               /* large desktop */
  --pg-gap-x: 36px;           /* horizontal rhythm */
  --pg-gap-y: 32px;           /* vertical rhythm, a shade tighter than the gutter */
  padding: calc(var(--header-h) + var(--top-pad)) var(--margin) var(--margin);
  min-height: 100vh;
}
@media (max-width: 1200px) { .playground { --pg-cols: 3; } }
@media (max-width: 1000px) { .playground { --pg-cols: 2; } }
@media (max-width: 750px)  { .playground { --pg-cols: 1; } }

.playground__grid {
  display: flex;
  align-items: flex-start;
  gap: 0 var(--pg-gap-x);
}

.playground__col {
  flex: 1 1 0;
  min-width: 0;
}

/* carries the height of the items virtualised away above this column, so
   pruning can drop DOM nodes without moving a single pixel on screen */
.playground__spacer { width: 100%; }

.playground__item {
  margin: 0 0 var(--pg-gap-y);
  /* the resting state of the reveal: displaced, a shade small, barely turned */
  opacity: 0;
  transform: translateY(50px) scale(.94) rotate(2.3deg);
  transition: opacity .6s cubic-bezier(.16, 1, .3, 1),
              transform .7s cubic-bezier(.16, 1, .3, 1);
  will-change: opacity, transform;
}
/* alternate the tilt so the page never leans one way. Items no longer share a
   single parent, so the alternation travels as a modifier, not :nth-child */
.playground__item--flip { transform: translateY(50px) scale(.94) rotate(-2.3deg); }
/* once an item has arrived, release the compositing layer: leaving
   will-change set pins every item to its own layer for good, which is
   what was rasterising the stills a touch soft */
.playground__item.is-in { opacity: 1; transform: none; will-change: auto; }

/* the occasional narrow item is what keeps the columns from reading as a grid.
   Centred, so the slack either side reads as gutter rather than as a hole */
.playground__item--small {
  width: 55%;
  margin-left: auto;
  margin-right: auto;
}

.playground__item img,
.playground__item video {
  display: block;
  width: 100%;
  height: auto;               /* native proportions, never cropped */
  filter: grayscale(1);       /* the archive reads as one material */
  transition: filter 350ms ease;
}

/* colour is a pointer reward, so it is asked for only where a pointer exists */
@media (hover: hover) and (pointer: fine) {
  .playground__item:hover img,
  .playground__item:hover video { filter: grayscale(0); }
}

/* an unfilled slot: a labelled box at the ratio the real file will have */
.playground__empty {
  aspect-ratio: var(--pg-ratio, 1 / 1);
  display: flex;
  align-items: flex-end;
  background: #f2f2f2;
  padding: 10px;
  font-size: var(--fs-meta);
  line-height: var(--lh-meta);
  color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  .playground__item,
  .playground__item--flip { opacity: 1; transform: none; transition: none; }
  .playground__item img,
  .playground__item video { transition: none; }
}

/* --------------------------------------------------------------------------
   12. Responsive
   Breakpoints follow the reference: a tablet range and a 750px mobile switch.
   -------------------------------------------------------------------------- */
@media (max-width: 1000px) {
  .project {
    grid-template-columns: 26fr 74fr;
    column-gap: 24px;
  }
  .index { grid-column: 1; }
  .meta  { grid-column: 1; grid-row: 2; position: static; margin-top: 48px; max-width: none; }
  .media { grid-column: 2; grid-row: 1 / span 2; }

  .about { grid-template-columns: 34fr 66fr; row-gap: 40px; }
  .about__portrait { grid-column: 1; grid-row: 1; }
  /* stacked: the list moves up into the row the label used to hold, so no
     empty row is left behind */
  .about__services { grid-column: 1; grid-row: 2; }
  .about__statement { grid-column: 2; grid-row: 1 / span 2; }

  .contact { position: static; margin: 96px 0 40px; padding: 0 var(--margin); }
}

@media (max-width: 750px) {
  :root {
    /* The page margin is the one number the whole mobile layout hangs off.
       26px read as a wide band next to 12-15px type; 16px puts the visible
       left, right and top gaps on the same value and hands the difference
       back to the four navigation items. */
    --margin: 16px;
    /* header band = 14px of top padding + the 19px nav row + the same 12px of
       slack underneath that the 57px desktop band carries. Everything that
       clears the header (project, about, the project index, the close button)
       is expressed in this token, so the nav and the content below it move up
       together and no gap between them changes. */
    --header-h: 45px;
    /* One size for the navigation and for the page's own headings and contact
       details, so mobile has a single reference size instead of two. */
    /* One size for the whole mobile page: the navigation, the About services
       and statement, and the contact block. Two points below the curve it
       carried before (11.99px at 393px, 9.98px now), with a 9.5px floor so
       the smallest screens do not fall further. */
    --fs-mobile: clamp(9.5px, 2.54vw, 12px);
    /* five points up from the body size: the word-mark in the bar, the three
       destinations in the menu and the project names all share it */
    --fs-mobile-lg: calc(var(--fs-mobile) + 5px);
    /* the desktop token takes 4px off the margin to cancel the descent of the
       last line; at this size the descent is smaller, and 3px puts the ink the
       same distance from the foot of the screen as from its left edge */
    --contact-b: calc(var(--margin) - 3px);
    /* The step of air between the project names on the index, on top of the
       line box. One value, used by that list and by the menu, so the two
       cannot drift apart. */
    --list-gap: calc(var(--fs-mobile-lg) * 0.38);
    /* The menu's first line is a fixed point in this layout - the project
       index is hung from it - so the list is centred in the screen as a box
       of this height and its items flow from the top of that box. The height
       is the one the list had when that point was set: three line boxes and
       two of the old 1.9 steps. Changing the air between the items now
       changes what happens inside the box, not where its top line lands. */
    --menu-box-h: calc(3 * var(--fs-mobile-lg) * var(--lh-tight)
                       + 2 * (var(--fs-mobile-lg) * 1.9));
    --fs-nav: 15.4px;
    --fs-body: 15.4px;
    --fs-meta: 15.4px;
    --fs-index: 15.4px;
    --top-pad: 11px;
    --gap-media: 20px;
  }

  /* The cap of the type sits below the padding edge - further below now that
     the smaller type leaves more room inside the 19px nav row - while the "G"
     has only 0.75px of side bearing. Taking 4px off the side margin puts the
     three visible gaps back on the same measure. */
  .site-header { padding-top: calc(var(--margin) - 4px); }

  /* The bar carries the full name and the menu button, nothing else: the three
     words move into the sheet the button opens. */
  .nav__name-full  { display: inline; }
  .nav__name-short { display: none; }
  .nav__center,
  .nav__end { display: none; }

  /* the name is the one piece of type in the bar, at the larger size */
  .site-header a.nav__left { font-size: var(--fs-mobile-lg); }
  /* the row grows with the type instead of clipping it */
  .site-header__inner { height: auto; }

  /* Three thin rules, on the same right margin `about` used to end on, and
     centred against the name. Open, the outer two cross and the middle one
     goes - the same button becomes the close mark, in the same place. */
  .nav__burger {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    pointer-events: auto;
    position: relative;
    width: 22px;
    height: 13px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--fg);
    -webkit-tap-highlight-color: transparent;
  }
  .nav__burger span {
    display: block;
    width: 100%;
    height: 1px;
    background: currentColor;
    transition: transform .28s cubic-bezier(.33,1,.68,1), opacity .18s ease;
  }
  body.is-menu-open .nav__burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
  body.is-menu-open .nav__burger span:nth-child(2) { opacity: 0; }
  body.is-menu-open .nav__burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

  /* The sheet: one screen of the page's own white, the three destinations
     stacked on the page margin, nothing else. It sits above the page but
     under the header, so the button that opened it closes it. */
  .mobile-menu {
    position: fixed;
    inset: 0;
    z-index: 180;
    background: var(--bg);
    display: flex;
    align-items: center;
    padding: 0 var(--margin);
  }
  .mobile-menu[hidden] { display: none; }
  /* The row has to be the link's own line box and nothing else. The list
     item was still inheriting the body's 15.4px, whose strut is a pixel
     taller than the link's line box and would make the step between the
     destinations a pixel longer than the step between the project names. The
     link sets its own size, so this changes no visible type. */
  .mobile-menu__list {
    width: 100%;
    height: var(--menu-box-h);
    font-size: var(--fs-mobile-lg);
    line-height: var(--lh-tight);
  }
  /* The same air between the destinations as between the project names. The
     menu sets its lines on --lh-tight and the index on --lh-bio, so the gap
     value alone would leave the menu a fraction closer; adding the difference
     between the two leadings makes both the white between the letterforms and
     the step from one line to the next identical on the two screens. The
     project-index value is reused here, not copied. */
  .mobile-menu__list li + li {
    margin-top: calc(var(--list-gap)
                     + (var(--lh-bio) - var(--lh-tight)) * var(--fs-mobile-lg));
  }
  .mobile-menu__list a {
    display: inline-block;
    font-size: var(--fs-mobile-lg);
    line-height: var(--lh-tight);
    color: var(--fg);
    position: relative;
  }
  body.is-menu-open { overflow: hidden; }
  /* `projects` keeps an auto margin on both sides so the width left over in
     the bar is split evenly in front of and behind it, instead of piling up
     on one side and leaving the word-mark and `projects` nearly touching. */
  /* `projects` rejoins the flex row. It stays *relative* rather than static so
     that it is still its own containing block: its 44px touch overlay would
     otherwise resolve against .site-header__inner, stretch across the whole
     bar and swallow the tap on the word-mark. left/transform are reset so the
     relative box sits exactly where the flow puts it. Desktop is untouched -
     there .nav__center is still absolute and centred on the viewport. */
  .nav__center {
    position: relative;
    left: auto;
    transform: none;
    margin: 0 auto;
  }

  .project,
  .about {
    display: block;
    padding-top: calc(var(--header-h) + var(--top-pad));
  }

  /* reference stacking order on mobile: media first, then the text block */
  .project { display: flex; flex-direction: column; }
  .index { display: none; }                  /* reachable through the drawer */
  .media { order: 1; padding-bottom: 0; }
  /* the information column joins the one mobile type size the rest of the
     site runs on; the leading is a ratio, so it follows by itself */
  .meta  {
    order: 2;
    position: static;
    max-width: none;
    margin-top: 40px;
    font-size: var(--fs-mobile);
  }

  .playground__item--small { width: 70%; }   /* still breaks the rhythm at one column */

  /* ----------------------------------------------------------------------
     About: the desktop composition, compressed to two bands.
     Desktop reads portrait | services | statement across one row, with the
     contact block in the bottom-left corner. A phone cannot carry three text
     bands side by side, so the two word blocks share the right-hand band and
     stack in reading order, while the portrait keeps the left band to itself
     - exactly as it does on desktop, where the area under the photograph is
     empty until the contact block. Proportions come from the desktop page:
     the portrait-to-services gap is 53px of a 1392px content box there, which
     is the 14px used here; the portrait is widened from desktop's 19% because
     19% of a phone is a thumbnail.
     ---------------------------------------------------------------------- */
  .about {
    display: grid;
    /* The left band holds the photograph and, under it, the services. 38% of
       the content box everywhere it is wide enough for the longest service
       ("graphic design", 10.93em in this face); on the narrowest phones that
       word is wider than 38%, so the track takes the word's width instead and
       nothing in the list can wrap. */
    grid-template-columns: max(38%, calc(var(--fs-mobile) * 11.1)) 1fr;
    /* Row 1 is the photograph, row 2 the services, row 3 the flexible
       remainder. The statement spans all three: because it spans a flexible
       track, it adds nothing to the height of rows 1 and 2, so the services
       stay tucked under the photograph however long the text runs. */
    grid-template-rows: auto auto 1fr;
    /* portrait -> services: the page's own left margin, so the air between the
       photograph and the list measures the same as the air between the
       photograph and the edge of the screen */
    column-gap: var(--margin);
    /* portrait -> services: the page's own left margin */
    row-gap: var(--margin);
    align-items: start;
    /* the block keeps min-height: 100vh from the base rule; without this the
       spare height is handed to the auto rows and opens a gap between the
       services and the statement */
    align-content: start;
    /* the photograph's top gap to the navigation now measures the same as the
       page margin at its sides */
    padding-top: var(--header-h);
  }

  /* the left band: photograph, then the services directly beneath it */
  .about__portrait { grid-column: 1; grid-row: 1; }
  /* width 100% of a 38% track - the 265 x 336 box, the cover crop and the
     centred framing all come from the base rule, so the photograph keeps its
     exact proportion and character, only smaller */
  .about__portrait img { width: 100%; }

  /* The right band of the top row: the list sits on the photograph's own top
     line - both are row 1 and the grid aligns its items to the start - and is
     set at the head of its track, so the gap in front of it is the grid's
     column gap - the page margin, the same interval that stands between the
     photograph and the left edge of the screen. The list keeps its own size
     and leading. */
  .about__services {
    grid-column: 1;
    grid-row: 2;
    justify-self: start;
    /* The row gap is the page margin, but a line box carries half of its
       leading above the letters, so 16px of box would read as 17-18px of
       visible air against the hard edge of the photograph. Taking that half
       line back - the block's own leading, not a number - puts the top of the
       letterforms the same distance below the picture as the picture stands
       from the left edge of the screen. Same reasoning as --contact-b. */
    margin-top: calc(-1 * (var(--lh-bio) - 1) * var(--fs-mobile) / 2);
    font-size: var(--fs-mobile);
    line-height: var(--lh-bio);
  }

  .about__statement--full  { display: none; }
  .about__statement--short { display: block; }

  /* The statement leaves the grid and takes the foot of the screen, on the
     same fixed anchor the contact block used to hold: hung from the viewport
     itself, on the page margin at the left and at the foot. The contact block
     took 3px off the bottom to cancel the descent below its last baseline;
     this block ends on a line whose descenders run to the foot of the line
     box, so the full margin is what measures equal - 16.5px of visible gap at
     the left and 16.5px underneath, read off the rendered pixels rather than
     the box. Both paragraphs run on in one block in the corner. Its measure
     is the one the pair of columns was set to, so the line length of the text
     does not change and the block stays a block in the bottom left rather
     than a band across the whole foot of the screen. */
  .about__statement {
    position: fixed;
    left: var(--margin);
    right: auto;
    bottom: var(--margin);
    width: calc((100vw - 3 * var(--margin)) / 2);
    margin: 0;
    max-width: none;
    padding-bottom: 0;
    font-size: var(--fs-mobile);
    line-height: var(--lh-bio);
    text-align: left;
  }
  /* stacked, with the blank line between the paragraphs measured off the
     block's own size */
  .about__statement p { margin: 0; }
  .about__statement p + p { margin-top: calc(var(--fs-mobile) * var(--lh-bio)); }

  /* The desktop block is `215px auto`: the value column hugs its content and
     the first column carries the slack that separates them. Same logic here,
     with 1fr in place of the fixed 215px so the separation grows with the
     screen instead of being a number tuned to a 1440 canvas. The four groups
     stay in their original order and read as a 2 x 2 block, as on desktop. */
  /* Four groups, still 2 x 2 and still in their original order, but sized to
     sit quietly in the corner rather than to fill the page: both columns hug
     their content, the separation between them is the same 3-ish em the
     desktop block carries, and the whole thing shrinks to its own width
     instead of stretching to the margins. */
  .contact {
    grid-template-columns: auto auto;
    column-gap: calc(var(--fs-mobile) * 2.6);
    row-gap: 22px;
    width: max-content;
    max-width: 100%;
    margin: 72px 0 48px;
    font-size: var(--fs-mobile);
  }
  .contact__label { margin-bottom: 6px; }

  /* the page's own headings sit on the navigation's size */
  .home__mark { font-size: var(--fs-mobile); }

  .home { min-height: 100vh; }
  .home__mark { top: 44%; left: var(--margin); }
  .home__deco { display: none; }
  /* the trail stays: on a phone a finger drives it instead of a cursor */
}

/* --------------------------------------------------------------------------
   Project description: anchored bottom-left on desktop, mirroring the project
   index's top-left position (same left margin, same offset from the edge).
   Scoped to the desktop breakpoint so the stacked layouts below 1000px keep
   their existing static placement untouched.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   13. Small screens and touch input
   Everything below is additive and scoped: the 1001px+ desktop layout above
   is never reached by any of these queries.
   -------------------------------------------------------------------------- */

/* The phone Playground keeps the desktop character - two offset streams packed
   by height, not rows - rather than collapsing to a single column. The streams
   are widened by pulling in the Playground's own side padding; the global
   --margin the rest of the site aligns to is untouched.
   At 393px: (393 - 16 - 26) / 2 = 175.5px per stream. */
@media (max-width: 750px) {
  .playground {
    --pg-cols: 2;
    --pg-gap-x: 26px;
    --pg-gap-y: 40px;         /* two narrow streams need more air than four wide ones */
    padding-left: 8px;
    padding-right: 8px;
  }
  /* at this width a 70% item reads as a mistake rather than a rhythm break */
  .playground__item--small {
    width: 100%;
    margin-left: 0;
    margin-right: 0;
  }
}

/* --- mobile Projects: a plain index, then a detail view with a way back ---- */
@media (max-width: 750px) {
  /* the index view: names only, nothing else rendered */
  .project.is-index .meta,
  .project.is-index .media { display: none; }

  /* The index sits on the same grid as the About page: the list starts on the
     page margin and on the same line below the header that the About
     photograph starts on, and the page is one screen, as About is. */
  /* The list hangs from the foot of the screen instead of the header: the
     view is a full-height column and the names are pushed to its end, so the
     block keeps its left margin and takes the same measure as its distance
     from the bottom edge. */
  .project.is-index {
    padding-top: var(--header-h);
    min-height: 0;
    height: 100%;
  }

  /* The list keeps the fixed box it was given - placed against the viewport
     itself, so no viewport unit, page height or safe-area inset is involved -
     but it is now hung from the menu's first line rather than from the foot of
     the screen. The menu centres its list in the same fixed box, so half of
     that list's height above the centre line is exactly where `About` starts;
     the second term is the difference between the two leadings, which puts the
     ink of the first project name on the ink of `About` rather than its line
     box on its line box. Nothing here is a measured top value: change the menu
     and this follows it. */
  .project.is-index .project-index {
    position: fixed;
    left: var(--margin);
    right: var(--margin);
    top: 50%;
    bottom: auto;
    transform: translateY(calc(-1 * var(--menu-box-h) / 2
                               - (var(--lh-bio) - var(--lh-tight))
                                 * var(--fs-mobile-lg) / 2));
    width: auto;
  }

  body:has(.project.is-index) .page {
    min-height: 100vh;
    min-height: 100dvh;
    height: 100vh;
    height: 100dvh;
  }

  /* A touch drag can rubber-band the document even when it fits; the same
     treatment Home and About carry takes that away. Only the index view is
     locked - the detail view underneath it still scrolls. */
  html:has(.project.is-index),
  html:has(.project.is-index) body {
    height: 100%;
    overflow: hidden;
    overscroll-behavior: none;
  }
  html:has(.project.is-index) .project { touch-action: pinch-zoom; }

  .project-index { width: 100%; }
  /* The names run on the About services' rhythm exactly: the same size, the
     same leading, and no gap of their own between rows, so one line box is
     the whole step from one name to the next - the value is the services'
     value, not a new one. Each row is a block, so the row rather than the
     word is what a finger has to find. */
  /* A step of air on top of the leading. The block is hung from its first
     line now, so the height it takes is given downwards and the top of the
     list stays where the menu puts it. Eased back a little from 0.45 - about
     a pixel a row at 393px - which keeps the rhythm open and nowhere near the
     bare line box the names ran on before. */
  .project-index li + li { margin-top: var(--list-gap); }
  .project-index__link {
    display: block;
    font-size: var(--fs-mobile-lg);
    line-height: var(--lh-bio);
    color: var(--fg);
    position: relative;
  }

  /* the 34px that used to hold the corner clear for the close control goes
     with it: the view starts on the same line every other page does */
  .project.is-detail { padding-top: calc(var(--header-h) + var(--top-pad)); }

  /* description first, media underneath: the visitor reads what the project is
     before scrolling through it. This flips the order the stacked mobile
     layout has used until now. */
  .project.is-detail .meta  { order: 1; margin-top: 0; }

  /* The description runs as two columns of the same width with the page
     margin between them, so it reads as two editorial blocks rather than one
     wide one. The paragraphs keep their order - the first column fills, then
     the second - and no paragraph is allowed to break across the gutter, so
     the two columns land where the copy naturally divides instead of being
     forced to the same height. */
  .project.is-detail .meta__desc {
    column-count: 2;
    column-gap: var(--margin);
  }
  .project.is-detail .meta__desc p { break-inside: avoid; }
  /* The split description: two independent blocks side by side, each as tall
     as its own copy. Nothing flows between them and no height is set on
     anything, so no line can be cut off at the foot of one column or the head
     of the next. The row is as tall as the taller block, and the media's own
     top margin puts the first image the usual page margin below it. */
  .project.is-detail .meta__desc[data-split] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--margin);
    align-items: start;
    column-count: auto;
  }
  .project.is-detail .meta__desc[data-split] .meta__desc-col { display: block; }
  .project.is-detail .meta__desc[data-split] .desc-m { display: block; }
  .project.is-detail .meta__desc[data-split] .desc-d { display: none; }
  .project.is-detail .meta__desc[data-split] .meta__desc-col p:first-child { margin-top: 0; }
  /* Long copy: the block is filled instead of balanced, so the first column
     takes the share the script sets and the second ends higher. The break is
     allowed inside a paragraph here - with this much text a line break across
     the gutter is ordinary typesetting, and it is the only way the difference
     between the columns can be slight rather than a whole paragraph. */
  .project.is-detail .meta__desc.is-uneven { column-fill: auto; }
  .project.is-detail .meta__desc.is-uneven p { break-inside: auto; }
  /* the paragraph a project names in `descBreak`: it opens the second column
     instead of starting at the foot of the first one */
  .project.is-detail .meta__desc p.is-col-break { break-before: column; }
  /* The blank line between paragraphs is already carried on the top of the
     following one, so the bottom margin only added a trailing band under the
     column - which is what the wide gap before the first image was made of. */
  .project.is-detail .meta__desc p { margin-bottom: 0; }
  /* The document already ends on the last media's own bottom edge, so the
     white the eye reads underneath it is the page margin and nothing else. */
  /* text to media: the page margin, the same measure as the sides */
  .project.is-detail .media { order: 2; margin-top: var(--margin); padding-bottom: var(--margin); }


}

/* Below ~360px the three nav items need 315px but only 268px exists between
   the margins, so `about` was being clipped off the right edge. `playground`
   steps out of the bar — it is already the second entry in the drawer — and
   the remaining two keep their exact positions and type size. */
/* The header type is sized for the phone rather than inherited from the
   desktop scale: ~14px at 393px, never below 13px, never above 15px. Scoped to
   .site-header so the contact block and the drawer keep their own size. */
@media (max-width: 750px) {
  .site-header a { font-size: var(--fs-mobile); }
  /* nothing in the bar may be squeezed narrower than its own word, or the
     name compresses and clips instead of the row simply being too long */
  .site-header a,
  .nav__end { flex-shrink: 0; white-space: nowrap; }
  /* `playground` and `about` stay a pair, as they are on desktop, so their
     own gap is the tighter of the two; the flex row hands any width left
     over to the two gaps on the left. */
  /* `playground` and `about` are the only pair whose gap is not fed by the
     row's auto margins, so it is set to what those margins work out to at
     393px - the three gaps in the bar then read as one even rhythm. */
  .nav__end { gap: 40px; margin-left: 0; }
  .site-header__inner { gap: 6px; }
}

/* PP Monument Extended is a wide face: the four words alone measure 24.06px of
   ink per 1px of type, so four items at the 12.5px floor need 300.7px of text
   plus 20px of gaps - 373px of usable width, i.e. a 375px viewport. Below that
   `playground` steps out of the bar (it is the second entry in the drawer) and
   the remaining three go back up to 14px, which fits with room to spare even
   on a 320px screen. */
@media (max-width: 374px) {
  /* the type is no longer stepped up here - one curve covers every width, so
     only the bar's fourth item steps out */
  .nav__end > a:first-child { display: none; }
}

/* --------------------------------------------------------------------------
   Mobile Home and About: one screen each, no scroll.
   Both pages are exactly one dynamic viewport tall (dvh, so the iOS toolbars
   appearing and disappearing cannot turn them into scrolling pages), and on
   both the contact block takes the desktop treatment - pinned to the bottom
   left corner on the page margin - so it takes no height out of the flow and
   its gap to the left edge and to the foot of the screen are the same. The
   project pages and the Playground are untouched and still scroll.
   -------------------------------------------------------------------------- */
@media (max-width: 750px) {
  body[data-page="home"] .page,
  body[data-page="about"] .page {
    min-height: 100vh;
    min-height: 100dvh;
    height: 100vh;
    height: 100dvh;
  }

  body[data-page="home"] .home,
  body[data-page="about"] .about {
    height: 100%;
    min-height: 0;
  }

  /* --contact-b is the margin less 4px: the last line's descent makes up the
     difference, so the ink sits the same distance from the foot of the screen
     as it does from the left edge. Home only: on About the statement holds
     that corner now and the four groups are off the page. */
  body[data-page="home"] .contact {
    position: fixed;
    left: var(--margin);
    bottom: var(--contact-b);
    max-width: calc(100% - 2 * var(--margin));
    margin: 0;
    padding: 0;
  }

  /* The phone's About page drops the contact block entirely - it is out of the
     flow, not merely moved, so nothing it used to occupy is left behind. The
     details are untouched on Home and on every desktop width. */
  body[data-page="about"] .contact { display: none; }

  /* The pages already fit the viewport exactly, but a touch drag can still
     rubber-band the whole document on iOS, which reads as the screen sliding
     under the finger - on Home it fights the image trail. overflow +
     overscroll-behavior take the bounce away, and `touch-action: pinch-zoom`
     tells the browser not to treat a drag over the page as a pan at all -
     touchmove still fires, so the trail is unaffected, taps on the navigation
     and the contact links are untouched, and pinch-to-zoom is deliberately
     left working. */
  html[data-page="home"],
  html[data-page="home"] body,
  html[data-page="about"],
  html[data-page="about"] body {
    height: 100%;
    overflow: hidden;
    overscroll-behavior: none;
  }

  html[data-page="home"] .home,
  html[data-page="home"] .contact,
  html[data-page="about"] .about,
  html[data-page="about"] .about__statement { touch-action: pinch-zoom; }
}

/* The right-hand home decoration is anchored at 93.54% + 90px, which lands
   past the viewport between the mobile switch and roughly 1100px and gave the
   Home page a real sideways scroll on tablets. It is decorative, so it simply
   sits out that range; at 1101px and above it is untouched. */
@media (min-width: 751px) and (max-width: 1100px) {
  .home__deco--b { display: none; }
}

/* The trail is no longer gated on the input device: it follows a cursor where
   there is one and a finger where there is not. Only reduced motion opts out,
   which the component itself honours. */

/* Touch hit areas. Each rule adds an invisible, absolutely positioned overlay
   centred on the link — it takes no space, so not one visible pixel moves and
   every measured alignment on the page is preserved. */
@media (hover: none), (pointer: coarse) {
  /* `projects` is absolutely centred on the viewport and is already its own
     containing block - giving it position:relative here would drop it back
     into the flex row and move it. Only the statically placed links need it. */
  /* .project__close is already position:fixed and is its own containing block;
     listing it here would reset it back into the flex column. */
  .site-header a:not(.nav__center),
  .nav__burger,
  .mobile-menu__list a,
  .drawer__close,
  .project-index__link,
  a.contact__value,
  .meta__link,
  .drawer__list a { position: relative; }

  .site-header a::after,
  .nav__burger::after,
  .mobile-menu__list a::after,
  .drawer__close::after,
  .project__close::after,
  .project-index__link::after,
  a.contact__value::after,
  .meta__link::after,
  .drawer__list a::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: max(100%, 44px);
    height: 44px;
  }

  /* The drawer rows sit on a 35px pitch (21px of text + a 14px gap). A 44px
     target would overlap its neighbour and steal taps, so these fill the pitch
     exactly instead: the whole list becomes tappable with no dead bands and no
     two targets ever overlap. */
  .drawer__list a::after { height: 35px; }

  /* Same reasoning, tighter pitch: the project names now sit one line box
     apart, so a 44px target would cover three of its neighbours. The overlay
     fills its own row instead - full width, full height, no overlap. */
  .project-index__link::after { width: 100%; height: 100%; }
}

@media (min-width: 1001px) {
  .meta {
    position: fixed;
    top: auto;
    left: var(--margin);
    bottom: calc(var(--margin) + 1px);   /* +1px so the VISIBLE text gap matches the left text ink */
    width: 462px;
    max-width: 462px;
  }
  /* the last paragraph's trailing 16.5px margin sat inside the block and
     pushed the visible text up, making the bottom gap read as ~35px against
     an 18px left gap. Dropping it lets the text end exactly on the offset. */
  .meta__desc p:last-child { margin-bottom: 0; }
  /* that rule now also catches the last paragraph of the FIRST container,
     which is not the end of the copy; it keeps its blank line */
  .meta__desc-col:not(:last-child) > p:last-child { margin-bottom: 16.5px; }
}

