/*
Theme Name: Paloma Dray
Theme URI: https://github.com/Hay-and-Rice/Palomadray-Theme
Author: Hay & Rice
Author URI: https://hayandrice.co.uk
Description: Custom portfolio theme for palomadray.com.
Version: 1.0.0
Requires PHP: 8.1
Text Domain: palomadray
*/

/* ---------- tokens ---------- */
:root {
  /* #ffebf5, sampled off the three landing mockups in FEEDBACK 31 august
     rather than guessed — "possible to see how is it . with pink color bg
     please?". The page was #fbf4f7, which is pink but so pale it read as
     off-white beside her comps. */
  --bg: #ffebf5;
  --ink: #000;
  /* Paloma's corner, 25 of her 6984-unit board, as a share of whatever
     container the media sits in. Declared once because it belongs to every
     image and video in her content, not to project pages alone. */
  --media-radius: 0.358cqi;
  --ink-soft: #272727;
  /* FEEDBACK 2 sept draws every pill — HOME, the dots, the opened row — as
     rgba(221,220,224,0.2) on a white board, and "if you can make it glassy
     transparent" is the Slack note that came with it. On the site the pills
     float over artwork, and at her 20% the #272727 label measured 1.25:1 over
     a dark hero on a phone. 60% white is the least that keeps it readable
     (5.2:1 over black) while staying as faint on the pink page as hers is on
     white (1.03:1 against the page, hers 1.04:1); the frost is the blur on
     the pills themselves. Was #ffdfee, the solid pink of the earlier comps. */
  --pill: rgba(255, 255, 255, 0.6);
  --font: "Jost", "Futura", sans-serif;

  /* One value for every page that opens with content below the pills. The comp
     starts the services list and the first project image within a pixel of each
     other here; only about starts higher, and that is the page carrying 18 lines
     and a portrait, so it reads as having been pulled up to fit its frame rather
     than as a second rule. */
  --page-top: 250px;

  /* Outer margin of the grid matches the gutter between its tiles. The comp is a
     pixel and a half wider at the edges, which is below what can be measured off
     it, and one gutter is easier to reason about than two. */
  --grid-gutter: 12px;
}

/* The deck, the grid overlay over it and the category archives are all one
   browsing surface — the archives literally reuse .grid-view. The comp put that
   surface on white while reading pages stayed pink, but FEEDBACK 31 august
   draws all three landing frames on the same #ffebf5 as everything else, so the
   distinction goes and the pink runs right through. Kept as a token override
   rather than a property so .grid-view follows without knowing. */
body.home,
body.tax-portfolio_category {
  --bg: #ffebf5;
}

/* The deck loops, so its scrollbar would measure a document the reader can
   never reach the end of, and its thumb would jump on every wrap. The grid
   hides its own for the same reason; the reference has no scrollbar at all. */
body.home { scrollbar-width: none; }
body.home::-webkit-scrollbar { display: none; }

/* 400 arrived with the blog (6 Sept 2026): its comps set body copy and every
   secondary line in Jost Regular, and without the file the browser was
   substituting the 500. Nothing else on the site asks for it. */
@font-face {
  font-family: "Jost";
  src: url("https://www.palomadray.com/wp-content/themes/palomadray/assets/fonts/jost-v20-latin-regular.woff2") format("woff2");
  font-weight: 400;
  
;font-display:swap;}
@font-face {
  font-family: "Jost";
  src: url("https://www.palomadray.com/wp-content/themes/palomadray/assets/fonts/jost-v20-latin-500.woff2") format("woff2");
  font-weight: 500;
  
;font-display:swap;}
@font-face {
  font-family: "Jost";
  src: url("https://www.palomadray.com/wp-content/themes/palomadray/assets/fonts/jost-v20-latin-700.woff2") format("woff2");
  font-weight: 700;
  
;unicode-range:U+0-1F,U+7F-2012,U+2015-2018,U+201A-201B,U+201E-10FFFF;font-display:swap;}

/* ---------- base ---------- */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-weight: 500;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

/* ---------- header pills ---------- */
.pill {
  position: fixed;
  top: 30px;
  z-index: 70;
  display: inline-flex;
  align-items: center;
  /* 30px tall, per FEEDBACK 3 sept: "INCREASE SIZE h:30 px" with an arrow at
     HOME and the back link. Her drawing is the number and the type together —
     the "Home" she marks is 44px of capitals against our 38.2 at 12px, which is
     14px type — and that is the size the phone has been since 2 sept. So
     desktop and phone now differ by 2px of height rather than by a step.
     Was 5px 16px on 12px type, ~28 tall; before that 37.5, which sat outside
     every reading I could take off the comp. */
  padding: 4px 16px;
  min-height: 30px;
  border-radius: 500px;
  background: var(--pill);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: var(--ink-soft);
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  text-decoration: none;
  letter-spacing: 0.02em;
}
/* ---------- magnetic chrome (assets/js/magnet.js) ----------
   The pills, the two corner discs and the opened menu row lean up to 4px
   toward a nearby pointer.
   Following is quick; letting go is slower and overshoots a little, so the
   button settles back rather than snapping. The script sets is-drawn while the
   pointer is within the halo. translate rather than transform: see the script. */
.pill,
.view-toggle,
.project-close,
.site-menu__list {
  transition: translate 0.5s cubic-bezier(0.34, 1.56, 0.64, 1),
    scale 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.pill.is-drawn,
.view-toggle.is-drawn,
.project-close.is-drawn,
.site-menu__list.is-drawn {
  transition-duration: 0.12s, 0.5s;
  transition-timing-function: ease-out, cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Pressed: the button gives a little under the pointer or the thumb, and
   springs back on release on the same curve the lean returns on. A control
   with no press state reads as unresponsive; this is the whole of it. The
   opened menu presses link by link rather than as a row — the link is the
   thing being pressed — and it is the one press the phone has too. scale,
   like translate, composes with the landing's transform. */
.pill:active,
.view-toggle:active,
.project-close:active,
.site-menu__list a:active {
  scale: 0.96;
  transition-duration: 0.12s, 0.1s;
  transition-timing-function: ease-out, ease-out;
}
/* Sides, not destinations: which two of Projects/About/Contact appear depends
   on the page (header.php), so the class can only describe where it sits. */
.pill--left { left: 30px; }
.pill--right { right: 30px; }

/* ---------- the left of the header ----------
   HOME alone on most pages, HOME and the back link on a project. Same shape as
   .site-menu opposite: the wrapper is what is fixed, and the pills inside it
   are laid out by the flex row rather than each knowing where the other ends.
   position: relative rather than static so the phone's 8px halo, which is an
   ::after on the pill, has something to size itself against — with top and left
   cancelled, since those are meant for a pill that positions itself. */
.site-home {
  position: fixed;
  top: 30px;
  left: 30px;
  z-index: 70;
  display: flex;
  align-items: center;
  /* 18px, off her drawing: HOME ends at 110 and the back link starts at 129.
     Not the 12 the menu uses opposite — that is the space between the dots and
     the row they open, which is a different join. */
  gap: 18px;
}
.site-home .pill { position: relative; top: auto; left: auto; }
/* The arrow is hers, and decorative — the label carries the meaning. */
.pill--back [aria-hidden] { margin-right: 0.35em; }
/* One label or the other, never both: whichever is display:none is out of the
   accessible name too, so the link reads as exactly what it shows. */
.pill--back__short { display: none; }

/* ---------- header menu ----------
   The three-dots half of the header. .pill is fixed, so the wrapper takes the
   fixed position instead and the toggle goes back to static inside it —
   otherwise the list has nothing to anchor to and lands at the top of the
   document. */
.site-menu {
  position: fixed;
  top: 30px;
  right: 30px;
  z-index: 70;
  display: flex;
  /* Reversed so the dots stay on the right edge and the list opens leftward
     from them, on the same line as HOME. */
  flex-direction: row-reverse;
  align-items: center;
  gap: 12px;
  /* On the wrapper so the dots and the list share one ink. */
  color: var(--ink-soft);
}
.site-menu__toggle {
  position: static;
  right: auto;
  border: 0;
  cursor: pointer;
  font: inherit;
  /* The dots are 4.6px on a 30px pill, so the usual 16px sides would crowd
     them. */
  padding: 4px 14px;
  min-height: 30px;
}
.site-menu__dots {
  display: block;
  /* Exactly three: 4.6px cells repeated across a 14px box. A fourth fits at
     16px, and she drew three.

     FEEDBACK 3 sept marks "INCREASE SIZE" at the dots with no number beside it,
     unlike the 30px she gives the pills. So they take the step the phone took
     on 2 sept — 12x4 to 14x4.6 — which is the same 15% and now applies at every
     width, so there is one set of dots rather than two. */
  width: 14px;
  height: 4.6px;
  /* Three dots as one background rather than three elements — currentColor so
     it follows the pill's text colour without a second token. */
  background-image: radial-gradient(currentColor 50%, transparent 55%);
  background-size: 4.6px 4.6px;
  background-repeat: repeat-x;
}
/* Dots to cross, in place. The dots are a background on a 12x4 box; the cross
   is two bars drawn from the box's centre, so it sits where the dots were and
   the pill keeps its shape. Closing is a press on the thing that opened it. */
.site-menu__dots { position: relative; }
.site-menu__dots::before,
.site-menu__dots::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 1.5px;
  border-radius: 2px;
  background: currentColor;
  transform: translate(-50%, -50%) rotate(45deg) scaleX(0);
  transition: transform 0.2s ease;
}
.site-menu__dots::after { transform: translate(-50%, -50%) rotate(-45deg) scaleX(0); }
.site-menu.is-open .site-menu__dots { background-image: none; }
.site-menu.is-open .site-menu__dots::before { transform: translate(-50%, -50%) rotate(45deg); }
.site-menu.is-open .site-menu__dots::after { transform: translate(-50%, -50%) rotate(-45deg); }
@media (prefers-reduced-motion: reduce) {
  .site-menu__dots::before,
  .site-menu__dots::after { transition: none; }
}
/* Opened sideways: one pill reading PROJECTS CONTACT SERVICES, to the left of
   the dots on the same line. FEEDBACK 2 sept drew the row under the dots with
   a cross beside it; the cross is the dots now, and the row sits beside them
   rather than beneath. The link widths are hers. */
.site-menu__list {
  display: flex;
  flex-direction: row;
  align-items: center;
  /* Her links are 107/94/98 units wide around 64-unit labels, so the gap is
     the padding of two neighbouring links rather than a margin of its own. */
  gap: 4px;
  margin: 0;
  padding: 5px 6px;
  list-style: none;
  border-radius: 500px;
  background: var(--pill);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  /* The same 14px and 30px the pills take from FEEDBACK 3 sept. It is not a
     .pill itself — it is a row of links sharing one background — so it does not
     inherit them, and it sat a step smaller than HOME beside it until this. */
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  min-height: 30px;
}
.site-menu__list[hidden] { display: none; }
.site-menu__list a {
  display: inline-flex;
  align-items: center;
  align-self: stretch;
  padding: 0 10px;
  text-decoration: none;
  color: inherit;
  white-space: nowrap;
  /* Same lozenge as the footer row. A little more white than --pill here,
     because this row already sits on a frosted pill rather than on the page,
     and the two at the same value barely separated. */
  border-radius: 500px;
  transition: background-color 0.22s ease, color 0.22s ease,
    scale 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.site-menu__list a:active { transition-duration: 0.22s, 0.22s, 0.1s; }
.site-menu__list a:hover,
.site-menu__list a:focus-visible {
  background: rgba(255, 255, 255, 0.75);
  color: var(--ink);
}


/* ---------- homepage deck ---------- */
/* --slot is the scroll pitch — one project per slot. It is twice the 9.8vh
   visual unit home.js's OFFSETS table is written in (mirrored there as
   visualSlot): OFFSETS spreads the cards beside the focused one nearly two
   visual units apart, so a pitch of one unit moved the deck at ~2x the
   trackpad gesture and felt slippery. Doubling the pitch puts the focused
   card back at ~1:1. It is not where a card ends up: the comp spaces the
   deck unevenly, so home.js paints each card off its slot. */
.deck {
  --slot: max(144px, 19.6vh);
  /* The no-JavaScript layout: a plain column of cards, first and last centred.
     home.js replaces it with .deck-stage and .deck-spacer below. */
  padding: calc(50vh - var(--slot) / 2) 0;
  /* The deck drags with the mouse as well as scrolling (home.js). Cards keep
     the link pointer — clickability reads louder than draggability there. */
  cursor: grab;
}
/* The cards are pinned to the viewport and placed entirely by transform, and
   the spacer behind carries the scroll length. Nothing here moves with the
   document, which is the point: a card left in the flow is positioned by the
   compositor's scroll offset and a transform computed from the main thread's,
   and on a trackpad those two disagree frame to frame. See home.js.

   The cards not in the fan are parked a viewport or more above the stage,
   where they are not painted, their animations do not tick, and their lazy
   images stay unfetched until they are wanted. The stage used to clip them
   with overflow: hidden as well; that had to go, because a clip flattens a 3D
   context and the stacking below depends on one.

   fixed, not sticky. The stage was sticky inside .deck, and on WebKit the
   loop's wrap — a scrollTo of a whole loop, painting identically either side —
   blanked the entire deck for two frames as the sticky offset caught up, the
   "flash white at the stop" on an iPhone. Filmed in the Simulator: two frames
   of bare page at every wrap, none with the wrap off, none with the stage
   fixed. A fixed stage does not move with the scroll at all, so a jump in the
   scroll cannot displace it. The spacer grows by a viewport to make up for
   the stage leaving the flow (home.js, layoutLoop).

   preserve-3d, because the cards are stacked by translateZ rather than
   z-index (home.js, update). There is no perspective, so the depth moves
   nothing on screen — it only decides which card paints over which. A z-index
   change is a repaint in WebKit, and rewriting it as the deck scrolled had
   Safari re-rasterising cards every half a slot; a transform is not. */
.deck.is-pinned { padding: 0; }
.deck-stage {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  transform-style: preserve-3d;
}
/* While a drag is live the pointer can cross cards and chrome; keep the fist
   everywhere until release (home.js toggles the class). */
body.deck-dragging,
body.deck-dragging * { cursor: grabbing; }
/* Laid out at the focused width and scaled down from there, so the browser sizes
   the image for what is actually shown. Laying out small and scaling up made
   sizes="auto" pick a 128w candidate for a 432px card.

   Back to the comp's landscape card. The square was an experiment and the
   feedback rejects it three times over ("keep rectangle view and not square").
   The comp's whole deck is traced from the reference site, whose focused card
   is 900x540 with the stack running 495/414/333/252 — all 5:3, and the exact
   ladder SCALES and OFFSETS already encode. Only the ratio had drifted. */
.deck-card {
  display: block;
  /* "Could you please increase the size of the boxes here?", against a box she
     drew at 60% of the viewport. Three readings agree: her box measures 907 on
     a 1512 viewport, the site she marked EXEMPLE runs 59-61%, and the reference
     this deck was traced from has a 900x540 focused card, which is 59.5vw at
     1512. Only the base width was ever wrong — SCALES already carries that
     reference's own 495/414/333/252 ladder. 1100px keeps the cap at the same
     viewport width the old 768px did against 42vw. */
  width: min(60vw, 1100px);
  min-width: 300px;
  margin: 0 auto;
  height: var(--slot);
  position: relative;
}
.deck-card img {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  width: 100%;
  height: auto;
  aspect-ratio: 5 / 3;
  object-fit: cover;
  /* 6px at the old 3.57x focused scale; kept visually identical now scale is 1. */
  border-radius: 21px;
  /* Featured images can be transparent PNGs; without a background the deck
     cards stacked behind would show through them. */
  background: #fff;
  /* The comp's card halo — a hairline shadow ring, not a border. Deck cards
     only: the comp's grid tiles and project heroes carry no shadow. It is
     what keeps a white-background cover from bleeding into the white page.

     No blur. It was 0 0 4px 1px at 0.25, and on iOS the blur was the other
     half of the deck's stutter: whenever Safari repaints a card, a blurred
     shadow is a full Gaussian pass over the card's area at 3x, where a spread
     with no blur is a rounded stroke. Measured in the Simulator, the flat
     ring alone took the deck from 18-21 long frames over three drags to 7. */
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.16);
}
/* Subtle grow on hover, echoing the grid's zoom. On the img rather than the
   card: home.js owns the card's transform and rewrites it every frame, so a
   hover transform there would be clobbered mid-scroll. Off while dragging —
   cards sliding under a held pointer shouldn't pulse. */
@media (hover: hover) {
  .deck-card img { transition: transform 0.3s ease, opacity 0.3s ease; }
  body:not(.deck-dragging) .deck-card:hover img,
  body:not(.deck-dragging) .deck-card:hover video { transform: translateY(-50%) scale(1.03); }
}
/* The video's own transition lives with the rest of its rule further down, not
   here. Listing it in this block silently did nothing: `.deck-card video` there
   carries the same specificity and comes later in the file, so it won last and
   left the video with no transform transition at all. The video then snapped
   between the two scales while the image underneath eased over 300ms, and for
   that third of a second the image stood ~7px proud of the video on every side,
   showing its corner and its shadow ring around the playing card. */

/* ---------- view toggle ---------- */
/* Offset by 22 rather than 30 so the disc's edge lands roughly on the 30px
   corner line the pills already sit on. Setting this to 30 pushed the whole
   control out and left the two corners disagreeing.

   Identical to .project-close, deliberately. Clicking a project swaps this
   control for that one in the same corner, and when the box, the offset and the
   disc all match, the only thing that changes across the navigation is the
   glyph — which reads as one button changing state rather than two buttons
   trading places. It needs the disc on its own account anyway: the grid view is
   images wall to wall, and a bare icon in that corner has the same legibility
   problem the close button just had. */
.view-toggle {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 80;
  width: 44px;
  height: 44px;
  padding: 11px;
  border: 0;
  background: var(--bg);
  border-radius: 50%;
  cursor: pointer;
}
.view-toggle img { width: 100%; height: 100%; }

/* ---------- grid view ---------- */
.grid-view {
  position: fixed;
  inset: 0;
  z-index: 60;
  overflow-y: auto;
  background: var(--bg);
  padding: var(--grid-gutter);
}
/* Front-page view state. Desktop opens on the deck, mobile on the grid, and an
   explicit toggle wins at either width. Scoped to #grid-view so the always-on
   category archive, which reuses .grid-view, is left alone. The default lives in
   CSS rather than JS so neither view flashes before home.js runs. */
#grid-view { display: none; }
body.view-grid #grid-view { display: block; }
body.view-grid { overflow: hidden; }

.view-toggle .icon-deck { display: none; }
body.view-grid .icon-grid { display: none; }
body.view-grid .icon-deck { display: block; }

/* ---------- deck ⇄ grid transition ---------- */
/* home.js names the focused card project-hero on both sides of the toggle, so
   the browser morphs that one card between its deck position and its grid cell
   while the rest of the page crossfades. Browsers without view transitions
   just get the instant swap. */
::view-transition-group(project-hero) {
  animation-duration: 0.45s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
/* The two sides are the same photo at the same aspect ratio, differing only
   in corner radius (21px card vs 4px cell). Keep the default crossfade: the
   UA blends old over new with plus-lighter, which on identical pixels is a
   no-op, so all it visibly does is ease the corners from one radius to the
   other. Snapshots stretched to the group so both radii scale together. */
::view-transition-old(project-hero),
::view-transition-new(project-hero) {
  width: 100%;
  height: 100%;
}

/* The deck fades out and the grid fades in, matched to the card fade the grid
   uses when it opens without a transition to ride on — one length for the same
   moment either way. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.35s;
}

/* Page to page crossfades too (@view-transition, further down), but only the
   root: the card → hero morph was tried and reverted twice, because it always
   reshapes awkwardly (the cards are cover-cropped, the hero is natural ratio).
   home.js names the focused card project-hero only for the duration of the
   toggle, so a navigation never finds a pair to morph. */

.grid-list {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--grid-gutter);
  list-style: none;
  margin: 0;
  padding: 0;
}
.grid-list a {
  display: block;
  /* The comp's cell is 494x297 and the deck card is 5:3; the two have to match
     exactly or the view transition between them reshapes instead of just
     easing the corner radius. 5:3 for both, to the comp's rounding. */
  aspect-ratio: 5 / 3;
  border-radius: 4px;
  overflow: hidden;
  /* relative so the hovered card can sit above its neighbours as it grows */
  position: relative;
  transition: transform 0.3s ease;
  /* Transparent PNG featured images would otherwise show the page (and, while
     a hovered neighbour grows over them, that card) through the tile. */
  background: #fff;
}
.grid-list a:hover {
  transform: scale(1.04);
  z-index: 1;
}
.grid-list img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---------- cover videos on the deck cards ----------
   The same box as the card's still, laid over it. Invisible until home.js has
   it playing, so the still is the poster; nothing else about the card moves.
   Once it has shown a frame it stays: moving the deck pauses it on that frame
   rather than fading it back to the still (home.js, startVideo). */
.deck-card video {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  width: 100%;
  height: auto;
  aspect-ratio: 5 / 3;
  object-fit: cover;
  border-radius: 21px;
  opacity: 0;
  pointer-events: none;
  /* The image's own background and halo, repeated here so the card keeps both
     once the image is taken out from under a playing video. */
  background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.16);
  /* transform as well as opacity: the hover grow is applied to the image and
     the video together, so they have to ease together too. */
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.deck-card video.has-frame { opacity: 1; }
/* Once the video is fully opaque, take the image out from under it. Both carry
   the same box and the same 21px radius, so along the straight edges the video
   covers the image exactly — but at the four corners the two anti-aliased
   curves cannot coincide, and the image's edge blended out around the video's
   as a hairline of the wrong colour: white against a dark video, dark against
   a light one.
   visibility rather than opacity, and delayed by exactly the fade: opacity
   would cross-fade, and two elements each at half alpha let the page show
   through between them. Hiding at 0.3s means the video is already solid when
   the image goes. */
.deck-card:has(video.has-frame) img {
  visibility: hidden;
  transition: transform 0.3s ease, visibility 0s linear 0.3s;
}

/* A cover video on a grid cell. Same shape as the deck's, at the cell's own
   4px corner, and built by home.js only for the handful of cells it has chosen
   to play — see the conductor there for why it is not every cell. */
.grid-col a video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 4px;
  opacity: 0;
  pointer-events: none;
  background: #fff;
  transition: opacity 0.3s ease;
}
.grid-col a video.has-frame { opacity: 1; }

/* ---------- the homepage grid's endless columns ---------- */
/* home.js rebuilds .grid-list into a sticky stage of columns, each an endless
   strip travelling against its neighbours as you scroll — the reference moves
   odd columns up and even columns down by equal amounts, and stops dead when
   the input does. Without JavaScript none of this happens and the flat
   .grid-list stays an ordinary wall, which is also what the category archives
   keep using. */
.grid-view.is-looping {
  padding: 0;
  /* The thumb would measure a loop the page doesn't really have, and jump on
     every wrap. The columns are the scroll indicator. */
  scrollbar-width: none;
}
.grid-view.is-looping::-webkit-scrollbar { display: none; }
.grid-view.is-looping .grid-list { display: none; }

.grid-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  grid-template-columns: repeat(var(--cols, 5), 1fr);
  gap: var(--grid-gutter);
  padding: var(--grid-gutter);
  /* Columns run past both ends of the viewport by design. */
  overflow: hidden;
}
.grid-col { position: relative; }

/* ---------- the grid's entrance ----------
   The feedback's "when user lands on this page, an animation", in two beats:
   the grid fades in, then its columns travel. This is the first beat, and only
   for the cases with no view transition to ride on — opening straight onto the
   grid on a phone, or a browser without them. Through the toggle the root
   crossfade does the same job, at the same length.

   The keyframes still carry rise, scale and blur because an earlier version
   used all three. They are switched off below rather than deleted so the shape
   stays adjustable in one place; the travel in home.js is what carries the
   entrance now. */
@keyframes grid-card-in {
  from {
    opacity: 0;
    transform: translate3d(0, var(--card-rise), 0) scale(var(--card-scale));
    filter: blur(var(--card-blur));
  }
  to { opacity: 1; transform: none; filter: blur(0); }
}
/* Flat on purpose. This was a staggered, blurred, scaled cascade while it was
   the whole entrance. Once the columns' travel went in behind it there were two
   unrelated gestures back to back — one assembling, one travelling — and the
   first read as a flicker in front of the second. The reference simply fades:
   sixteen cards measured off the recording all reach half opacity within 25ms
   of each other, with no movement. */
.grid-stage {
  --card-rise: 0;
  --card-scale: 1;
  --card-blur: 0px;
  --card-dur: 0.45s;
  --card-spread: 0; /* no stagger — every card on the same clock, as hers are */
}
/* Only the cards on screen when the grid opens carry the animation. Every card
   in both strip copies would be 180 elements animating for nobody — this is 27
   on a laptop and 14 on a phone. home.js marks the visible ones. */
.grid-stage.is-entering .grid-col a.is-arriving {
  animation: grid-card-in var(--card-dur) cubic-bezier(0.22, 1, 0.36, 1) var(--card-delay, 0s) both;
}
@media (prefers-reduced-motion: reduce) {
  .grid-stage.is-entering .grid-col a.is-arriving { animation: none; }
}

/* ---------- arriving at the homepage ----------
   "when arriving on the homepage is it possible to have a small delay, first
   see the header (About and Contact button) and then the carousel with a small
   animation from the beginning". The recording she sent alongside it opens on
   an empty page, lands the wordmark, splits it to the edges, and only then
   flies the cards up into the stack. Same order here, against what this page
   actually has: the pills, then the toggle, then the deck rising into place.

   Every step is an animation with fill both, and is-landing is set from an
   inline script in the head. So the reveal never waits on home.js — if the
   deferred script is slow, or never arrives at all, these same keyframes still
   hand the page over on their own clock. The head is also the only place the
   class can go without the deck painting once before it is hidden.

   Nothing here animates a card's own transform: home.js writes that inline on
   every frame to paint the stack, and an animation would outrank it and freeze
   the deck. The cards carry opacity, the stage carries the movement. */
@keyframes landing-chrome-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes landing-card-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes landing-stage-in {
  from { transform: translateY(52px); }
  to   { transform: none; }
}
/* ---------- stage one: the mark alone ----------
   FEEDBACK 31 august draws the landing as three frames — the monogram, then
   "menus on the top and footer appears", then "all on screen". The mark is the
   frame that was missing; everything below is the old sequence pushed back
   behind it.

   The curtain holds 0.55s and fades over 0.35s, and the chrome starts at 0.75s
   so the two overlap rather than queue — a hard cut between frames reads as a
   page load, not an entrance. Total to first card is 1.05s against the old
   0.72s. */
@keyframes landing-mark-in {
  0%    { opacity: 0; transform: scale(0.94); }
  22%   { opacity: 1; transform: none; }
  61%   { opacity: 1; transform: none; }
  100%  { opacity: 0; transform: scale(1.04); }
}
.landing-mark { display: none; }
.is-landing .landing-mark {
  display: grid;
  position: fixed;
  inset: 0;
  place-items: center;
  z-index: 80;
  pointer-events: none;
  color: #d6066d;
  background: var(--bg);
  animation: landing-mark-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.is-landing .landing-mark img { width: 62px; height: auto; }

/* The pill, not the .site-menu wrapper: the toggle is a pill and fades on its
   own. Animating the wrapper's opacity made it a backdrop root — a descendant's
   backdrop-filter can only sample what its nearest translucent ancestor paints,
   so the dots never frosted the grid on a landing, and with fill both the
   isolation outlived the animation. */
.is-landing .pill {
  animation: landing-chrome-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.75s both;
}
.is-landing .view-toggle {
  animation: landing-chrome-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.95s both;
}
/* --card-delay is set per card by home.js, counted out from the middle of the
   stack, so the deck assembles from its centre rather than from whichever end
   the markup happens to start at. The fallback keeps a scriptless page working. */
.is-landing .deck-card {
  animation: landing-card-in 0.45s ease calc(1.05s + var(--card-delay, 0s)) both;
}
.is-landing .deck-stage {
  animation: landing-stage-in 0.95s cubic-bezier(0.22, 1, 0.36, 1) 0.99s both;
}
/* On a phone the homepage opens on the grid, so the wall of images is the thing
   that would otherwise be on screen before the pills. Held on the whole view
   rather than on its cards: the flat .grid-list is what shows until home.js has
   built the looping columns, and both live in here. The per-card entrance runs
   inside this, once. */
.is-landing .grid-view {
  animation: landing-card-in 0.4s ease 0.99s both;
}
@media (prefers-reduced-motion: reduce) {
  .is-landing .pill,
  .is-landing .view-toggle,
  .is-landing .deck-card,
  .is-landing .deck-stage,
  .is-landing .grid-view { animation: none; }
  /* The curtain has to go, not just stop animating: with fill both and no
     animation it would sit at its from-state and cover the page for good. */
  .is-landing .landing-mark { display: none; }
}

/* ---------- page to page ----------
   The browser's own cross-document transition: it snapshots the page being
   left, holds that snapshot until the new page can render, and crossfades the
   two. Both snapshots are complete pages, which is the whole point. Three
   passes at doing this by hand — a fade on the body on the way out, another
   on the way in, a class swapped when a page came back from the back-forward
   cache — all had the same failure in a real window: the fade out is cut off
   wherever the navigation happens to commit, the new page paints its first
   frame (blank pink, or white cards before the images decode) before any
   class can act on it, and a restored page is painted at full opacity once
   before pageshow runs. Every one of those is a frame that flashes. The
   transition has no such seam: nothing is shown until the new page can be
   shown, and it uses the same 0.35s root crossfade the deck ⇄ grid toggle
   already does.

   Closing a project does not go through it. That is a step back into the
   cached homepage (project-page.js), which is there in a frame, complete and
   on the card it was left on, and functions.php skips the transition for
   traversals because Chrome paints the restored page once before starting
   one. A cut between two finished pages. Browsers without any of this get the
   plain load. */
/* The opt-in itself — @view-transition { navigation: auto } — is not here. It
   is an inline <style> at the top of the head (functions.php): Chrome can run
   its check for the rule before an external stylesheet has arrived, and it did,
   every time, with this file delayed by 400ms — which is dev, where nothing
   static is cached. Inline, there is no race. */

@media (prefers-reduced-motion: reduce) {
  .footer-nav a,
  .site-menu__list a { transition: none; }
}

/* The strip that actually moves. One transform per column per frame, rather
   than one per card: the cards are evenly spaced, so translating the strip and
   letting it wrap is the same picture for a fraction of the style cost. */
.grid-col-inner {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  will-change: transform;
}
.grid-col a {
  display: block;
  aspect-ratio: 5 / 3;
  margin-bottom: var(--grid-gutter);
  border-radius: 4px;
  overflow: hidden;
  position: relative;
  transition: transform 0.3s ease;
  background: #fff;
}
.grid-col a:hover {
  transform: scale(1.04);
  z-index: 1;
}
.grid-col img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---------- project single ----------

   Every project comp is laid out on the same three widths, in the Figma
   board's own units:

     col   1200   the default - logo rows, pairs, standalone images
     mid   1764   the closing trio
     full  2328   the bleed images

   Those numbers are the grid below, used as fr, so the layout is the comp's
   own measurements rather than percentages someone rounded off them. 1200 plus
   282 twice comes to 1764 and then to 2328, which is why the space either side
   is two tracks and not one: mid stops at the inner pair, full at the outer.

   --side tapers those side tracks. On a wide screen it reaches 282/2328 of the
   board and the page has the comps' exact proportions; as the screen narrows it
   shrinks to nothing, so the column widens into the space instead of the whole
   page shrinking, and by the phone breakpoint all three widths have become one.
   Continuous rather than stepped, so nothing jumps mid-resize.

   --taper multiplies it, and is the one number to change if the column should
   be wider than Paloma drew it: 1 is the comp, 0 is a single full-bleed column,
   and everything between keeps the order of the three widths intact.

   0.45 is FEEDBACK 2 sept_02, "Please increase the size of the images here":
   her AFTER is the WIX page with 180px either side of the column on a 1512
   screen, which is 160 from the board's edge, two tracks of 80 — 0.45 of the
   282-unit track. Column 1152, mid 1312, full unchanged. */
.project {
  --taper: 0.45;
  /* "100px" with an arrow from the top of the AFTER frame to its first image;
     was the shared 250. Desktop only — the phone's 120 was not in question. */
  --project-top: var(--page-top);
  padding: var(--project-top) 20px 60px;
  /* width as well as max-width: the page is a column flex item, and auto inline
     margins on one of those switch off the stretch that would otherwise give it
     its width — with inline-size containment on top, that left it 40px wide. */
  width: 100%;
  max-width: calc(2328px + 40px);
  margin-inline: auto;
  /* The board the cqi units below are a fraction of. */
  container-type: inline-size;
}
.project-media {
  --side: calc(var(--taper) * clamp(0px, (100cqi - 860px) * 0.36, 12.113cqi));
  --gap: 0.5155cqi;   /*  12 / 2328 - the gap between images in a row */
  display: grid;
  grid-template-columns:
    [full-start] var(--side)
    [mid-start]  var(--side)
    [col-start]  minmax(0, 1fr) [col-end]
                 var(--side)
    [mid-end]    var(--side) [full-end];
}
/* Column unless the block says otherwise. Wide and full alignment in the
   editor's toolbar are the other two widths, so choosing one is the same
   gesture as in any WordPress theme. */
.project-media > * {
  grid-column: col-start / col-end;
  min-width: 0;
  margin: 0;
}
.project-media > .is-mid  { grid-column: mid-start / mid-end; }
.project-media > .is-full { grid-column: full-start / full-end; }
/* The space between one block and the next: 124 board units. A margin rather
   than the grid's row-gap, because a gap cannot be reduced for one pair and two
   consecutive paragraphs want a line between them, not an image's worth of air
   — a row-gap gave them both, one on top of the other.
   The reset above has to be one selector with this one, and first: written as
   `> figure { margin: 0 }` it out-specified this and every image block lost its
   top margin, which closed up two thirds of the page. */
/* 187 units now rather than 124: her AFTER scales the whole page up by 1.51
   and the space between blocks with it, 118px on a 1512 screen. */
.project-media > * + * { margin-top: 8.05cqi; }   /* 187 / 2328 */
/* Two rows running together are one contact sheet, not two blocks. Every project
   in the comps opens with a pair of eight-tile logo rows set 12 units apart —
   the same gap as between the tiles inside a row — so the sixteen read as one
   grid. At the block rhythm's 124 they read as two unrelated rows, which is
   what they have been doing on all five pages.

   Stated for any two adjacent rows rather than for the logo pair specifically:
   a row directly under a row is the only shape this can match, it is the only
   case the comps show, and reading them as one sheet is the right default for
   the next project too. The cost is that two rows cannot be held apart at the
   full rhythm without something between them; nothing in the comps asks for
   that, and the alternative is another choice in the sidebar. */
.project-media > .project-strip + .project-strip { margin-top: 0.515cqi; }  /* 12 / 2328 */
/* ...unless either of them is a carousel. The 12 units are what makes two
   matching logo rows read as one sheet of sixteen; a carousel is a different
   object at a different size, and at 12 units it sat almost touching the row
   below it. Back to the block rhythm on both sides — which is also what the
   reference does, where the carousel section takes the same 100px as every
   other section rather than a case of its own. */
.project-media > .project-strip.is-carousel + .project-strip,
.project-media > .project-strip + .project-strip.is-carousel { margin-top: 5.33cqi; }
@media (min-width: 901px) {
  .project { --project-top: 100px; }
}
/* The background is a way back: "click on the background and landing on the
   homepage". project-page.js follows the close button's link when the click
   lands on the page rather than on a block; the cursor says so. */
.project, .project-media { cursor: pointer; }
.project-media > * { cursor: auto; }
/* ---------- the project's title card ---------- */
/* FEEDBACK 2 sept: "when user clicks on a project, before seeing the projects
   to have the name of it appaearing?", drawn as the name centred on the
   project's own blurred backdrop and marked "1 - 3 sec delay".

   --intro-hold is the dial, and the only number worth moving. Everything else
   is derived from it, so her next revision of the timing is one value here
   rather than four. 1.6s sat in the middle of the range she drew; FEEDBACK 2
   sept_02 asks for the delay "to 2.5 sec", and what she is timing runs from
   the click — the page's own load is inside it — so the card holds 1s and
   fades over half a second, about 2.5s all in on the dev server.

   The backdrop is already behind the page and stays put; all this does is hold
   the page back and put the name on top of it while it waits. */
/* On :root, not .project — the card is a sibling of the page, not inside it,
   so a variable set on .project would never reach it. */
:root {
  --intro-hold: 1s;
  --intro-fade: 0.5s;
}
@keyframes project-intro-out {
  from { opacity: 1; }
  to { opacity: 0; visibility: hidden; }
}
@keyframes intro-name-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes project-page-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
.project-intro {
  position: fixed;
  inset: 0;
  z-index: 62;
  display: grid;
  place-items: center;
  padding: 0 24px;
  pointer-events: none;
  text-align: center;
  animation: project-intro-out var(--intro-fade, 0.55s) ease var(--intro-hold, 1.6s) both;
}
/* "so it takes a sec for the text to appear on it" — the name used to be at
   full strength in the first painted frame, which made it a caption on the
   backdrop rather than something arriving. It fades up instead.

   Inside the hold, not added to it: up by 0.5s, a beat at full strength, then
   the card's own fade out at 1s. The total from the click is unchanged, which
   matters — she asked for 2.5s and that is what the hold is set against. */
.project-intro span {
  animation: intro-name-in 0.38s ease 0.12s both;
  font-size: clamp(28px, 5.2vw, 64px);
  font-weight: 700;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
/* Held back for exactly as long as the card is up, then faded in under it. */
.project-media {
  animation: project-page-in var(--intro-fade, 0.55s) ease var(--intro-hold, 1.6s) both;
}
/* No card at all when motion is unwelcome: the delay is the effect, and a
   reader who has asked for less of it should not be made to wait through one. */
@media (prefers-reduced-motion: reduce) {
  .project-intro { display: none; }
  .project-intro span { animation: none; }
  .project-media { animation: none; }
}

/* No border-radius here on purpose, and squareness is not an export failure.
   The comps round some blocks and leave others square deliberately: across the
   file 95 blocks carry a 48-unit radius, 15 carry 72, and 13 carry none. The
   ones set to zero are the big standalone pictures — every project's hero, and
   all six of UEFA's blocks — while the small tiles inside a row are rounded
   generously (UEFA's brand tiles are 80). An export reproduces both, so 120 of
   the 145 images on the site arrive with a transparent rounded corner and 25
   arrive square, exactly as drawn.

   A blanket CSS radius therefore rounds 25 images their author squared and
   clips the other 120: measured at the size each one actually renders, the
   drawn radius runs from 7.5px to 47.9px, so anything large enough to show on
   the square ones cuts the corner off the rounded ones.

   "25 rounded" in FEEDBACK 31 august is not asking for a CSS rule. Every
   top-level block in the updated file now carries cornerRadius 25 — 123 of them
   across the original five, and all but a handful of the new ones — where the
   file we exported from months ago had 36, 48 and 72. She has standardised the
   corner and is telling us the site is behind it.

   And 25 there is 25 *board units*, not pixels: the full track is 6984 units
   wide and renders at 1472, so her corner is 5.3 CSS px. A border-radius in
   pixels would be that same number at every width — nearly five times her
   corner on a full-bleed block, a fraction of it on a tile the carousel has
   blown up 3.7x.

   So it is stated in cqi, the unit the rest of this file measures the board in:
   25 of 6984 is 0.358%, and .project is the inline-size container, so 0.358cqi
   is her corner at any viewport. The images are re-exported with cornerRadius
   zeroed in Figma — it is a frame clip there, not pixels in her PNGs — which
   puts the corner here rather than into 125 files, and makes her next change to
   it one number instead of another export run. */
.project-media img { border-radius: var(--media-radius); }
/* Except inside a carousel, where the card is a cropped window that already has
   its own radius and overflow:hidden. A second, smaller curve on the image
   would show the page through the gap between the two. */
.project-strip.is-carousel img { border-radius: 0; }
.project-media img { width: 100%; height: auto; display: block; }
/* A video is the same box as an image at the same alignment. aspect-ratio is
   written inline from the file's own dimensions, so the block holds its height
   before a byte of video arrives and nothing below it jumps. */
.project-media video { width: 100%; height: auto; display: block; border-radius: var(--media-radius); }  /* "rounded corners on video as well" */

/* A row is one line of images sharing a baseline: each takes the share of the
   row its own aspect is worth, so every height lands on the same number and the
   row fills its track exactly. The comps build all three of their rows this way
   - eight logo tiles, a pair of portraits, the closing trio - so one mechanism
   covers them and the only difference is how many images are in it.

   "see how the carousel is scrollable horizontal and also each images is
   separated not one bloc": the images stay separate files with the page's own
   background in the gaps, and below the phone breakpoint the row scrolls. */
.project-strip {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.project-strip::-webkit-scrollbar { display: none; }

/* ---------- carousel ----------
   The third row style, and the one the feedback's "see how the carousel is
   scrollable horizontal and also each images is separated not one bloc" is
   pointing at. Measured off yungbld.com/projects/freeze-frame — the same studio
   she gave as the reference in the first place — rather than approximated:

     desktop   item 28.5% of the container, gap 12px, radius 16px
     tablet    item 40%,                    gap  8px, radius 12px
     phone     item 40%,                    gap  6px, radius  8px

   aspect-ratio 5/7 and object-fit: cover at every width, overflow-x auto, the
   scrollbar hidden, and no scroll snap.

   Two numbers made this worth copying exactly rather than adapting. Their
   container at 1512 is the viewport less 40px of padding — 1472px, which is
   precisely our full track, so 28.5% is 419.5px in both. And her logo tiles are
   491x687, an aspect of 0.7147 against the 5/7 the reference uses, 0.7143: the
   crop takes nothing off them because the artwork is already that shape. The
   comps were plainly drawn from the same site.

   A justified row and a carousel are opposites — one fits the width by varying
   the widths, the other fixes the width and overflows — so --ar is ignored here
   rather than fought. */
/* Always the full track, whatever alignment the block carries. A carousel is
   defined by what it shows beside the card you are looking at, and in the column
   at 1512 that was 1.8 cards against the reference's 3.5 — the same card, the
   wrong idea. Full width makes 28.5cqi mean what it means on their page. */
/* Edge to edge, and past it. The reference's scroller is the whole viewport with
   20px of padding, so its cards begin 20px in and travel out under both edges;
   ours stopped at the full track, 20px short of each, and read as a panel rather
   than something running off the page. The negative margin is the same trick the
   phone breakpoint below already uses, at the same 20px, so the two agree where
   they overlap. */
.project-media > .project-strip.is-carousel {
  grid-column: full-start / full-end;
  margin-inline: -20px;
  padding-inline: 20px;
}
.project-strip.is-carousel { overflow-x: auto; cursor: grab; }
/* While dragging: no text selection, no ghost-dragging an image, and the cursor
   says the strip is being held. Mouse and pen only — a finger already has the
   better native gesture, so project-carousel.js leaves touch alone. */
.project-strip.is-carousel.is-dragging { cursor: grabbing; user-select: none; }
.project-strip.is-carousel img { -webkit-user-drag: none; user-select: none; }
/* No inset of its own. The reference pads its scroller by 20px because that
   scroller is the whole viewport; ours sits in a track that already has margins,
   and below 900 the rule above full-bleeds the strip and pads it — adding a
   second 20px here would have doubled it. */
.project-strip.is-carousel ul {
  display: flex;
  width: max-content;
  gap: 12px;
}
/* A common height, and each card as wide as its own picture needs — not the
   reference's uniform 5/7 box.

   Theirs can be uniform because their cards are photographs shot to one crop.
   Hers are artwork: seven of the eight tiles in this row are 0.715, which is
   5/7 to three decimals, but the eighth is a 1.499 landscape lockup, and
   object-fit: cover took "Sweet inn" off at both ends. contain would have been
   no better — these tiles carry their own background, so letterboxing shows the
   page through the gap.

   The height was the reference's card expressed the other way round — 28.5 x
   7/5, or 39.9cqi — which made a 5/7 tile exactly their 28.5cqi wide and the
   row theirs card for card. It is 27cqi now, and the reason is that the
   reference uses that card for a different kind of picture. freeze-frame's
   carousel holds seven big portrait photographs; the page she actually named
   for "an images bigger", /projects/vollie, lays rows of many small tiles out
   at 214px instead, justified and not scrolling at all. These rows are the
   second kind, so 39.9cqi was the wrong one of their two treatments.

   At 39.9cqi two and a half of the eight cards fit on a 1512 screen, which
   cannot read as a set, and two of these rows stack to 1252px — more than a
   viewport of logo tiles before any project work. The corner says the same
   thing: the tiles carry a drawn radius that lands at about 8.8px at the size
   the comps use, and scaling a tile scales its corner, so 39.9cqi renders it
   at 56px and 27cqi at 38px.

   Not lower, though: at 14.5cqi — the reference's own 214px — this row is
   exactly the width of the track and stops scrolling, which is the one thing
   the feedback explicitly asked for. 27cqi is still 3.7x the comps' 108px, so
   it is unambiguously bigger, and it overflows 1.8x so it plainly scrolls. */
.project-strip.is-carousel li {
  flex: 0 0 auto;
  width: auto;
  height: 27cqi;
  border-radius: 16px;
  overflow: hidden;
}
/* The one place a radius belongs. Everywhere else an image carries its own
   rounding with transparent corners and a CSS radius clips it; here the card is
   a cropped window onto the picture, so there is no baked corner underneath. */
.project-strip.is-carousel img {
  width: auto;
  height: 100%;
  object-fit: cover;
}
@media (max-width: 1024px) {
  .project-strip.is-carousel ul { gap: 8px; }
  /* 42cqi of a 794 container is 334px, against the desktop card's 397. These
     step down with the screen; at the reference's 56cqi a tablet card stood
     445px and so was taller than the desktop one, which is backwards. The
     phone keeps 56cqi below, where the container is small enough that the
     same share is a 196px card rather than a 445px one. */
  .project-strip.is-carousel li { height: 42cqi; border-radius: 12px; }
}
@media (max-width: 768px) {
  .project-strip.is-carousel ul { gap: 6px; }
  /* Back up to 56cqi, which is a bigger share of a much smaller container: 196px
     on a 390 screen, against the 147px the tablet's 42cqi would inherit here. */
  .project-strip.is-carousel li { height: 56cqi; border-radius: 8px; }
}
.project-strip ul {
  display: flex;
  gap: var(--gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.project-strip li { flex: var(--ar, 1) 1 0; min-width: 0; }
.project-strip img { width: 100%; height: auto; display: block; }

/* Copy reads at its own measure, centred in the column, rather than filling
   it or hugging its left edge. One measure in px for every text block: 66ch
   was measured in each block's own type, so a heading's box came out wider
   than a paragraph's, and once Paloma centred her text (6 Sept 2026, "i added
   text but it looks weird") every block centred on a different line, and a
   left-aligned paragraph sat against the column's edge beside them. Centred
   boxes give left, centred and right text one axis, which is what the editor
   canvas has always shown her. 680px is about 75 characters of the 18px copy,
   the blog post's measure. No project designed from the comps carries text,
   so nothing already built moves. */
.project-media > :is(p, ul, ol, h2, h3) { width: min(680px, 100%); margin-inline: auto; }
.project-media > :is(p, ul, ol) {
  font-size: 18px;
  line-height: 1.6;
}
.project-media > :is(p, ul, ol, h2, h3) + :is(p, ul, ol) { margin-top: 1em; }

/* ---------- the project page's blurred background ----------
   "on project opened, i d like to have a blurred background like here", against
   yungbld.com/projects/vollie, whose .frosty-bg is a full-bleed copy of the
   project image sitting behind everything at blur(40px) and half opacity. Hide
   it there and the page falls back to flat white, which is the whole effect.

   Inset negative by more than the blur radius: a filter fades to transparent at
   the edge of the element it is on, and this pushes that fade off screen so
   there is no vignette round the viewport. */
.project-backdrop {
  position: fixed;
  inset: -60px;
  z-index: 0;
  overflow: hidden;
  filter: blur(40px);
  /* Matched on the effect rather than the number. The reference uses .5, but
     over white behind a bright photo — measured across the frame, its backdrop
     darkens the surround by 21.5 luminance points out of 255, about 8.5%. The
     same .5 here dropped a near-black project image by 99 points and turned the
     whole page, footer included, mid grey. Reproducing their 21.5 wants .11 on
     a dark image and .21 on a light one, so .15 sits across both: 30 points
     down on the darkest project, 15 on a light one. */
  opacity: 0.15;
  pointer-events: none;
}
.project-backdrop img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The backdrop is fixed, so static content would paint under it. Everything
   above it needs a layer of its own; the pills at 70 and the close at 80
   already have one. */
.single-portfolio > main.project {
  position: relative;
  z-index: 1;
}
/* Above the bottom band as well as the backdrop: backdrop-filter samples only
   what paints beneath it, and a blurred copyright line reads as a fault. */
.single-portfolio .site-footer {
  position: relative;
  z-index: 66;
}

/* ---------- frosted edges ----------
   Chrome that floats has to sit on something. The pills and the close button
   are fixed, and since the images went full width below the breakpoint they
   travel underneath both — the pills alone cover 2,500 square pixels of a
   photograph at 768. Blurring a band at each edge is how the reference handles
   it, and it is what lets black type stay readable over whatever scrolls past.

   The mask fades each band out towards the middle of the screen so the blur has
   no edge of its own, and the top band carries a white wash as well, which is
   what actually buys the pills their contrast. */
.project-blur {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 65;
}
.project-blur span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 40px;
  /* Its own layer, or the blur re-rasterises against the page every frame. */
  transform: translateZ(0);
  backface-visibility: hidden;
}
.project-blur span::before,
.project-blur span::after {
  content: "";
  position: absolute;
  inset: 0;
}
.project-blur span::before {
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
.project-blur span:first-child { top: 0; }
.project-blur span:first-child::before {
  -webkit-mask-image: linear-gradient(#000 0, #000 50%, transparent);
  mask-image: linear-gradient(#000 0, #000 50%, transparent);
}
.project-blur span:first-child::after {
  background-image: linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
}
.project-blur span:last-child { bottom: 0; }
.project-blur span:last-child::before {
  -webkit-mask-image: linear-gradient(0deg, #000 0, #000 50%, transparent);
  mask-image: linear-gradient(0deg, #000 0, #000 50%, transparent);
}
/* No bottom band on a phone. Safari's own toolbar sits there, translucent,
   and a second frosted strip directly above it read as a fault — two blurs
   stacked, ours ending in a hard line where the toolbar's begins. The top
   band stays; nothing of the browser's lives up there. */
@media (max-width: 767px) {
  .project-blur span:last-child { display: none; }
}
@media (min-width: 768px) {
  .project-blur span { height: 80px; }
  .project-blur span::before {
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
  }
}
@media (min-width: 1024px) {
  .project-blur span::before {
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
  }
}

/* Back to the deck. The comp shows this in place of the header pills. */
/* Bottom right, where the homepage already puts its view toggle: same 40px box,
   same offset, same job, so the two finally read as one control in one place.
   It floated halfway down the right edge until now for a reason that has since
   gone — both top corners hold pills, and the bottom right held the footer's
   "Contact Paloma Dray", which the feedback had us remove.

   The disc is the page's own background rather than the pills' pink, so it
   reads as a hole punched through the artwork rather than a third pill. It is
   what makes a black cross survive a black photograph: measured on a
   four-image project at 390, the corner is over artwork from the first frame. */
.project-close {
  position: fixed;
  bottom: 22px;
  right: 22px;
  z-index: 80;
  width: 44px;
  height: 44px;
  padding: 11px;
  background: var(--bg);
  border-radius: 50%;
}
.project-close img { width: 100%; height: 100%; }

/* ---------- category archives ---------- */
/* A titled page, not the homepage wall: the service's name and its projects
   share the reading pages' 10.4vw margins and sit together, with the footer
   following the content rather than a forced viewport of empty space. Five
   columns like the wall: it was four, since the tiles start narrower, and
   Paloma asked (6 Sept 2026) for the five-wide grid everywhere. */
.category-view {
  padding: var(--page-top) 10.4vw 80px;
}
.category-title,
.category-empty {
  margin: 0 0 40px;
}
.category-title {
  font-size: clamp(24px, 2.2vw, 56px);
  line-height: 1.16;
  font-weight: 700;
  text-transform: uppercase;
}
.category-view .grid-list {
  grid-template-columns: repeat(5, 1fr);
}

/* ---------- the blog ----------
   From the two generated comps she sent 6 Sept 2026 ("i did something with
   ai": Figma ho8TEJM5Zskr7cgVKuqu3J, frames "Blog" and "Blog article"), drawn
   on a 2349-unit board. Every measurement below is that board's number times
   --u, one board unit as a share of the container, so the page is the comp
   scaled to the screen; type has a floor in px so a phone does not scale it
   into dust, and the phone rules at the bottom restack what a 390px screen
   cannot hold three-across.

   Its palette is its own — an ink, a soft grey, a magenta accent — and is kept to the two blog templates rather than promoted to
   tokens, because nothing else on the site uses any of it.

   Weights: the comp's SemiBold is 600, which the site does not carry, so it
   renders in the 700; Medium is the 500; Regular is the 400 added for this. */
.blog-index,
.article {
  --u: calc(100cqi / 2349);
  --journal-ink: #171217;
  --journal-soft: #6e626a;
  --journal-accent: #c81972;
  --journal-rule: #f22f93;
  --journal-panel: #d6066d;
  --journal-lime: #d9ff69;
  --journal-dark: #171217;
  width: 100%;
  max-width: 2349px;
  margin-inline: auto;
  container-type: inline-size;
  color: var(--journal-ink);
}

/* The hero: the Blog page's own content (an eyebrow paragraph, a heading, a
   paragraph) beside its featured image. Edited under Pages → Blog. The 80 on
   top is the comp's header band, which here is the fixed pills. */
.blog-hero {
  display: flex;
  align-items: center;
  gap: calc(104.4 * var(--u));
  padding: calc(210.5 * var(--u)) calc(156.6 * var(--u)) calc(130.5 * var(--u));
}
.blog-hero__copy { flex: 1 0 0; min-width: 0; }
.blog-hero__copy > * { margin: 0; }
.blog-hero__copy > * + * { margin-top: calc(35.888 * var(--u)); }
.blog-hero__copy > p {
  font-size: max(12px, calc(21.206 * var(--u)));
  font-weight: 700;
  letter-spacing: 0.12em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--journal-accent);
}
.blog-hero__copy > :is(h1, h2, h3) {
  font-size: max(36px, calc(117.45 * var(--u)));
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.03em;
}
.blog-hero__copy > :is(h1, h2, h3) ~ p {
  max-width: calc(880.875 * var(--u));
  font-size: max(17px, calc(34.256 * var(--u)));
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.5;
  text-transform: none;
  color: var(--journal-soft);
}
.blog-hero__image {
  flex: 0 0 calc(783 * var(--u));
  aspect-ratio: 783 / 685.125;
  border-radius: calc(45.675 * var(--u));
  overflow: hidden;
}
.blog-hero__image img { width: 100%; height: 100%; object-fit: cover; display: block; }

.blog-listing {
  padding: calc(39.15 * var(--u)) calc(156.6 * var(--u)) calc(182.7 * var(--u));
}
.blog-listing__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1em;
  margin-bottom: calc(58.725 * var(--u));
}
.blog-listing__title {
  margin: 0;
  font-size: max(28px, calc(61.988 * var(--u)));
  font-weight: 700;
  line-height: normal;
}
.blog-listing__count {
  margin: 0;
  font-size: max(14px, calc(22.838 * var(--u)));
  font-weight: 400;
  color: var(--journal-soft);
}

/* The card, on the index and under a post alike: picture, then category and
   date, title, excerpt and "Read article", on white with the comp's shadow.
   The whole card is the link. */
.post-grid {
  display: grid;
  /* Each card its own height, as the comp draws a row with a longer title. */
  align-items: start;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: calc(39.15 * var(--u));
  row-gap: calc(52.2 * var(--u));
  list-style: none;
  margin: 0;
  padding: 0;
}
.post-card { min-width: 0; }
.post-card__link {
  display: flex;
  flex-direction: column;
  background: #fff;
  border-radius: calc(26.1 * var(--u));
  overflow: hidden;
  box-shadow: 0 calc(19.575 * var(--u)) calc(52.2 * var(--u)) rgba(89, 18, 56, 0.09);
  color: inherit;
  text-decoration: none;
  transition: transform 0.3s ease;
}
.post-card__link:hover { transform: translateY(-4px); }
.post-card__media {
  display: block;
  position: relative;
  aspect-ratio: 652.5 / 404.55;
  background: #f6e3ec;
}
/* Absolutely placed so a tall picture is cropped to the box rather than
   stretching it: aspect-ratio alone is a preferred height, not a cap. */
.post-card__media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.post-card__summary {
  display: flex;
  flex-direction: column;
  gap: calc(22.838 * var(--u));
  padding: calc(39.15 * var(--u));
  text-align: left;
}
.post-card__meta {
  display: block;
  font-size: max(12px, calc(19.575 * var(--u)));
  line-height: normal;
}
.post-card__date { font-weight: 400; color: var(--journal-soft); }
.post-card__title {
  font-size: max(20px, calc(42.413 * var(--u)));
  font-weight: 700;
  line-height: 1.12;
}
.post-card__excerpt {
  font-size: max(15px, calc(24.469 * var(--u)));
  font-weight: 400;
  line-height: 1.5;
  color: var(--journal-soft);
}
.post-card__read {
  display: inline-flex;
  align-items: center;
  gap: calc(13.05 * var(--u));
  font-size: max(14px, calc(22.838 * var(--u)));
  font-weight: 700;
  line-height: normal;
}
.post-card__read img {
  width: max(16px, calc(26.1 * var(--u)));
  height: max(16px, calc(26.1 * var(--u)));
}
.blog-empty { margin: 0; font-size: 18px; }
/* Older / newer: WordPress's own paging, dressed as the site's pills. */
.blog-pages {
  display: flex;
  justify-content: center;
  gap: 18px;
  margin-top: calc(58.725 * var(--u));
}
.blog-pages a {
  display: inline-flex;
  align-items: center;
  height: 30px;
  padding: 0 16px;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  background: #fff;
  border-radius: 500px;
}

/* ---------- a post ---------- */
.article-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(39.15 * var(--u));
  padding: calc(223.55 * var(--u)) calc(309.938 * var(--u)) calc(78.3 * var(--u));
  text-align: center;
}
/* The comp's category-and-reading-time line, carrying the date instead —
   see single.php for why. */
.article-meta {
  margin: 0;
  font-size: max(12px, calc(21.206 * var(--u)));
  font-weight: 400;
  line-height: normal;
  color: var(--journal-soft);
}
.article-title {
  margin: 0;
  max-width: calc(1598.625 * var(--u));
  font-size: max(32px, calc(104.4 * var(--u)));
  font-weight: 700;
  line-height: 1.04;
  letter-spacing: -0.03em;
}
.article-standfirst {
  margin: 0;
  max-width: calc(1239.75 * var(--u));
  font-size: max(17px, calc(34.256 * var(--u)));
  font-weight: 400;
  line-height: 1.5;
  color: var(--journal-soft);
}
.article-hero { padding: 0 calc(156.6 * var(--u)) calc(130.5 * var(--u)); }
.article-hero figure {
  margin: 0;
  aspect-ratio: 2035.8 / 1145.137;
  border-radius: calc(45.675 * var(--u));
  overflow: hidden;
}
.article-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The body: one column, 1272 of the board, every block the comp's gap apart.
   The first paragraph is the lead. */
.article-body {
  width: min(calc(1272.375 * var(--u)), calc(100% - 40px));
  margin: 0 auto;
  padding-bottom: calc(156.6 * var(--u));
  font-size: max(17px, calc(27.731 * var(--u)));
  font-weight: 400;
  line-height: 1.7;
}
.article-body > * { margin: 0; }
.article-body > * + * { margin-top: calc(45.675 * var(--u)); }
.article-body > p:first-child {
  font-size: max(20px, calc(42.413 * var(--u)));
  font-weight: 500;
  line-height: 1.45;
}
.article-body > h2 {
  font-size: max(24px, calc(61.987 * var(--u)));
  font-weight: 700;
  line-height: 1.15;
}
.article-body > h3 {
  font-size: max(20px, calc(48 * var(--u)));
  font-weight: 700;
  line-height: 1.2;
}
.article-body > :is(ul, ol) { padding-left: 1.2em; }
.article-body a { text-decoration: underline; text-underline-offset: 0.12em; }
.article-body > blockquote {
  padding: calc(58.725 * var(--u)) calc(65.25 * var(--u));
  border-left: calc(8.156 * var(--u)) solid var(--journal-rule);
  border-radius: 0 calc(26.1 * var(--u)) calc(26.1 * var(--u)) 0;
  font-size: max(20px, calc(48.938 * var(--u)));
  font-weight: 500;
  line-height: 1.35;
}
.article-body > blockquote p { margin: 0; }
.article-body > blockquote p + p { margin-top: 0.6em; }
.article-body > blockquote cite {
  display: block;
  margin-top: 0.8em;
  font-size: max(12px, calc(19.575 * var(--u)));
  font-style: normal;
  font-weight: 400;
  color: var(--journal-soft);
}
/* "The takeaway": a paragraph given the Takeaway style in the editor. The
   label is the style's, not typed, so it is the same on every post. */
.article-body > .is-style-takeaway {
  padding: calc(52.2 * var(--u));
  border-radius: calc(26.1 * var(--u));
  background: var(--journal-panel);
  color: #fff;
  font-size: max(20px, calc(40.781 * var(--u)));
  font-weight: 500;
  line-height: 1.4;
}
.article-body > .is-style-takeaway::before {
  content: "The takeaway";
  display: block;
  margin-bottom: calc(22.837 * var(--u));
  font-size: max(12px, calc(19.575 * var(--u)));
  font-weight: 700;
  letter-spacing: 0.12em;
  line-height: normal;
  text-transform: uppercase;
  color: var(--journal-lime);
}
.article-body:lang(fr) > .is-style-takeaway::before { content: "À retenir"; }
.article-body > figure { --w: min(calc(2035.8 * var(--u)), calc(100vw - 40px)); }
.article-body img,
.article-body video {
  width: 100%;
  height: auto;
  display: block;
  border-radius: calc(26.1 * var(--u));
}
/* Wide and Full both take the comp's picture width, the hero's. */
.article-body > :is(.is-mid, .is-full) {
  width: var(--w);
  margin-inline: calc((100% - var(--w)) / 2);
}
.article-more { padding: calc(156.6 * var(--u)); }
.article-more__title {
  margin: 0 0 calc(58.725 * var(--u));
  font-size: max(28px, calc(75.037 * var(--u)));
  font-weight: 700;
  line-height: normal;
}

@media (max-width: 1100px) {
  .post-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 700px) {
  .post-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- big-type pages (about, services) ---------- */
/* Left margin stays at 10.4vw. The comp indents the services list to 16.26vw,
   but about — its only sibling — sits at 10.05vw, and nothing else in the file
   anchors 16.26: not a grid column edge, not centred on anything. One page
   disagreeing with its sibling and with everything else reads as a dragged
   block, so the two pages are kept aligned instead. */
.bigtype {
  padding: var(--page-top) 10.4vw 80px;
  /* The frame --media-radius is a share of, exactly as .project is for a
     project. Without it the unit has no container and falls back to the
     viewport, which is a different measurement that happens to look similar. */
  container-type: inline-size;
}
/* The same corner as a project's media. It used to stop at .project-media, so
   an image on About rendered square while the editor — whose canvas rounds
   every image — showed it curved. */
.bigtype-content :is(img, video) { border-radius: var(--media-radius); }
.bigtype-content {
  font-size: clamp(24px, 2.2vw, 56px);
  line-height: 1.16;
  font-weight: 700;
  overflow-wrap: break-word;
}
/* No text-transform here since 5 Sept 2026: a page shows what is typed, and
   the pages that existed then were put into capitals in the content itself
   (bin/uppercase-page-text.php). The services list is the one exception —
   its items are the category names, which are data used all over the admin,
   so the capitals stay a matter of paint. */
.services-list {
  text-transform: uppercase;
}
/* One blank line between paragraphs, as the comp shows after the opening one.
   Within a paragraph the copy uses <br>, which is why the rest of it runs on.
   Set to the line-height so the gap is exactly one line, and left off the first
   paragraph so the page still starts at --page-top. The image's paragraph
   collapses this into the larger margin on the image itself. */
.bigtype-content p { margin: 0; }
.bigtype-content p + p { margin-top: 1.16em; }
.bigtype-content a { text-decoration: underline; text-underline-offset: 0.12em; }
/* Roughly four lines of space below the copy. 8em was nearly double that —
   461px from the last baseline to the image where the comp shows 250 — and it
   was not anchored to anything. Kept in em so the gap tracks the type. */
.bigtype-content img { width: min(611px, 100%); margin-top: 4.25em; }

/* Lists in big-type content — the services page. page-services.php renders
   every portfolio category: one with projects is a link, and the underline it
   inherits from .bigtype-content a IS the design's visual language — the
   comp underlines exactly the services that open work. Empty categories
   render as plain unlinked text. */
.bigtype-content ul {
  list-style: none;
  margin: 0;
  padding: 0;
  text-underline-offset: 0.12em;
}

/* ---------- footer ---------- */
/* "On this page contact please put the footer on the bottomn of the page."
   Contact is three lines long, so in plain flow the footer sat halfway up an
   otherwise empty screen. Every page that ends in a footer now fills the
   viewport first and pushes it down. The front page is excluded because it has
   no footer and its own fixed overlays own the height. */
body:not(.home) {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
body:not(.home) > main { flex: 1 0 auto; }

.site-footer {
  padding: 40px 30px 30px;
}
.footer-nav {
  display: flex;
  justify-content: center;
  margin-bottom: 40px;
}
.footer-nav ul {
  display: flex;
  /* Five links at 16px of padding each measure 412px, which overflowed a 390px
     phone by 11px and gave every page with a footer a horizontal scroll. Wrap
     rather than squeeze: at this size the pill becomes two centred rows, and
     the padding stays legible. */
  flex-wrap: wrap;
  justify-content: center;
  max-width: 100%;
  list-style: none;
  margin: 0;
  padding: 0;
  border-radius: 34px;
  box-shadow: inset 10px 10px 40px 0 rgba(237, 237, 237, 0.75),
    inset 0 0 5px 0 rgba(255, 255, 255, 0.25);
}
.footer-nav a {
  display: inline-flex;
  align-items: center;
  height: 30px;
  padding: 0 16px;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-soft);
  border-radius: 500px;
  transition: background-color 0.22s ease, color 0.22s ease;
}
/* The hover the site already speaks: everywhere else it is a card growing a
   little under the pointer, which text cannot do without going soft. The row is
   a pill, so the answer is that the item under the pointer gets one of its own,
   in the same fill every other pill uses, and its ink goes to full black. No
   movement, nothing reflows, and it reads as the label lifting out of the row.

   An underline was the alternative and is what the header menu had. It is the
   browser's default idea rather than this site's, and next to a page built out
   of soft lozenges it looked like a link that had been forgotten. */
.footer-nav a:hover,
.footer-nav a:focus-visible {
  background: var(--pill);
  color: var(--ink);
}
/* Three tracks rather than space-between: the socials sit centred on the page,
   and the right-hand track is deliberately empty. "Contact Paloma Dray" used to
   fill it, and space-between would now shove the icons out to the right edge. */

/* Three tracks rather than space-between: the socials sit centred on the page,
   and the right-hand track is deliberately empty. "Contact Paloma Dray" used to
   fill it, and space-between would now shove the icons out to the right edge. */
.footer-bottom {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 20px;
  font-size: 18px;
  font-weight: 700;
}
.footer-social {
  display: flex;
  gap: 20px;
}
.footer-social img { width: 20px; height: 20px; }
.footer-bottom a { text-decoration: none; }

/* ---------- responsive ---------- */
@media (max-width: 900px) {
  /* Grid is the default here — the deck needs horizontal room a phone lacks.
     :not(.view-deck) means only an explicit switch to the deck overrides it.
     Scoped to .home: these are front-page view states, and an unscoped body
     rule would scroll-lock every other page on mobile. */
  body.home:not(.view-deck) #grid-view { display: block; }
  body.home:not(.view-deck) { overflow: hidden; }
  body.home:not(.view-deck) .icon-grid { display: none; }
  body.home:not(.view-deck) .icon-deck { display: block; }

  /* One column here: --side has already tapered to nothing by this width, so
     col, mid and full are the same track and every image fills the screen
     without a rule of its own. Rows go back to a fixed height and scroll
     sideways - justified, eight logo tiles would be 40px each - which is the
     behaviour the reference has throughout and what the feedback asked for. */
  .project-strip ul { width: max-content; gap: 12px; }
  .project-strip li { flex: 0 0 auto; }
  .project-strip img { width: auto; height: var(--strip-h); }
  .project-strip.is-logos { --strip-h: 132px; }
  /* A row on a phone is laid out by height and scrolls sideways, which is what
     the feedback asked for. But height alone decides width, and a fixed one
     turns a wide image into more than a screen on its own: GetResto's wordmark
     is 1.7:1, so at 380px it came out 644px against 350px of usable width and
     the reader landed on a word cut through the middle, with no scrollbar to
     say there was more. The row was not overflowing the page — it was scrolling
     without looking like it could.

     So the height is also capped by how wide it would make this particular row.
     --row-ar is the row's total aspect, written by render_block_core/gallery,
     and --row-screens is how many screens wide a row may be before the cap
     bites. Rows already narrower than that keep their height and nothing about
     them changes; only the wide ones come down. The 40px is the gutter the row
     bleeds into either side. Gaps are left out — they make the result a little
     wider than the target, which is the harmless direction.

     The fallback matters: a row with no --row-ar (an old cached render, or the
     editor before its script runs) divides by 99 and the cap falls away
     entirely, leaving the height exactly as it was. */
  .project-strip { --row-screens: 1.8; }
  .project-strip.is-large {
    --strip-h: min(52vh, 380px, calc(var(--row-screens) * (100vw - 40px) / var(--row-ar, 99)));
  }
  /* Bleeding into the gutters is what tells you the row scrolls; a row that
     stopped at the column edge would read as cropped instead. */
  .project-media > .project-strip { margin-inline: -20px; padding-inline: 20px; }

  /* 250px of it is a quarter of a phone screen, so the shared token comes down
     here and both templates follow. */
  :root { --page-top: 120px; }

  .grid-list { grid-template-columns: repeat(2, 1fr); }
  /* The archive's own four-column rule above outranks the bare selector, so
     it has to be repeated here or a phone shows four 68px tiles across. */
  .category-view .grid-list { grid-template-columns: repeat(2, 1fr); }
  /* The blog restacks: the hero's copy over its picture, the cards one
     above the other, and the comp's board-unit paddings replaced with the
     page's 20px, since a fraction of 390px is nothing. */
  .blog-hero { flex-direction: column; align-items: stretch; gap: 32px; padding: var(--page-top) 20px 40px; }
  .blog-hero__image { flex-basis: auto; width: 100%; border-radius: 16px; }
  /* The comp's measure widths are board units too, and a share of 390px is
     a column three words wide. */
  .blog-hero__copy > :is(h1, h2, h3) ~ p,
  .article-title,
  .article-standfirst { max-width: none; }
  .blog-listing { padding: 20px 20px 60px; }
  .blog-listing__head { margin-bottom: 24px; }
  .post-grid { column-gap: 16px; row-gap: 24px; }
  .post-card__link { border-radius: 16px; box-shadow: 0 12px 32px rgba(89, 18, 56, 0.09); }
  .post-card__summary { padding: 20px; gap: 12px; }
  .article-head { padding: var(--page-top) 20px 32px; gap: 20px; }
  .article-hero { padding: 0 20px 40px; }
  .article-hero figure { border-radius: 16px; }
  .article-body { width: calc(100% - 40px); padding-bottom: 60px; }
  .article-body > * + * { margin-top: 24px; }
  .article-body > blockquote { padding: 20px 24px; border-left-width: 4px; border-radius: 0 12px 12px 0; }
  .article-body > .is-style-takeaway { padding: 24px; border-radius: 16px; }
  .article-body > .is-style-takeaway::before { margin-bottom: 12px; }
  .article-body img, .article-body video { border-radius: 12px; }
  .article-more { padding: 0 20px 60px; }
  .article-more__title { margin-bottom: 24px; }
  .blog-pages { margin-top: 32px; }
  /* home.js reads the same breakpoint to decide how many strips to build. */
  .grid-stage { --cols: 2; }
  .bigtype { padding: var(--page-top) 24px 60px; }
  /* The type follows the phone's width rather than holding at 24px, so the
     longest line that cannot break — the email, 15em of capitals — always
     fits on one line: 22px on a 390 phone, 20px on a 360. Without this it
     broke mid-word ("CONTACT@PA / M"). The line is 15em long; 15.5 leaves
     the half-em of slack it needs, since an exact fit still wraps. A longer
     address moves the number. About and Services scale with it. */
  .bigtype-content { font-size: min(24px, calc((100vw - 48px) / 15.5)); }
  .footer-bottom { grid-template-columns: 1fr; justify-items: center; gap: 16px; }
  /* The focused card takes the width the project pages use — the screen
     minus the 20px margins — rather than the 300px floor of the desktop rule.
     Her reference's focused card fills its phone the same way, and the deck
     reads as a stack of one thing rather than a strip down the middle. The
     ladder either side scales with it (SCALES in home.js are fractions). */
  .deck-card { width: calc(100vw - 40px); min-width: 0; }
  .pill { top: 20px; }
  .pill--left { left: 20px; }
  .pill--right { right: 20px; }
  /* Neither pill positions itself any more: each sits in a wrapper that holds
     the fixed position, so both wrappers have to come in together or one side
     hangs lower and further in than the other. */
  .site-home { top: 20px; left: 20px; }
  .site-menu { top: 20px; right: 20px; }
  /* The one place the site needs a width other than 900. "← BACK TO PROJECTS"
     is 194px of pill: on a 360 it ran to 312 and the dots start at 294, so they
     overlapped outright, and even on a 390 the 18px between them is 2px once
     both halos are counted — the two press targets all but touch. Below 420px
     the link says BACK instead, which leaves 111px on a 390 and 81px on a 360.
     The label is swapped rather than shortened, so whichever one is on screen
     is the whole accessible name. */
  @media (max-width: 420px) {
    .pill--back__long { display: none; }
    .pill--back__short { display: inline; }
  }

  /* ---------- the menu on a phone ----------
     A pill is a fine thing to look at and a poor thing to press. Here it is
     32px rather than the 30 she asked for on desktop, and every pill grows an
     invisible 8px halo on top of that, so HOME and the dots are 48px tall to a
     thumb. The type and the dots are no longer part of this: 14px and 14x4.6
     were the phone's, and FEEDBACK 3 sept asked for both everywhere. */
  .pill {
    padding: 5px 18px;
    min-height: 32px;
  }
  .site-menu__toggle { padding: 5px 16px; }
  /* relative for the halo below, with the pill offsets cancelled — the toggle
     is a .pill too, and the 20px top and right meant for the fixed pills would
     otherwise shove it 20px down and in from the wrapper. */
  .site-menu__toggle { position: relative; top: auto; right: auto; }
  .pill::after {
    content: "";
    position: absolute;
    inset: -8px;
  }
  /* Two pills, stacked. Space is short sideways, so the pages drop down as
     a separate panel beneath the dots, right edges aligned, with the pill's
     own curve on its corners so the two read as a family. Each page is a
     full-width 48px line, which is what a thumb can land on. */
  .site-menu {
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
  }
  .site-menu__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    min-width: 200px;
    padding: 8px;
    border-radius: 14px;
    font-size: 14px;
  }
  .site-menu__list a {
    display: flex;
    min-height: 48px;
    padding: 0 12px;
    border-radius: 10px;
  }
  .site-menu__list a:active { background: rgba(255, 255, 255, 0.6); }
  /* 12 rather than 20, so the icons land on the 20px corner line the pills use. */
  .view-toggle { right: 12px; bottom: 12px; }
  /* Same corner as the toggle here too, or the button appears to hop when a
     project opens. */
  .project-close { right: 12px; bottom: 12px; }
}

/* On a phone the archive is the homepage wall, filtered: the same 12px gutter
   and the same two tiles across, so a tile is the size it is on the wall. The
   reading margin stays from a tablet up, where the tiles have room inside it. */
@media (max-width: 767px) {
  .category-view { padding-inline: var(--grid-gutter); }
}
